@chestnut23/dsh-conversation-outline 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/README.zh.md +13 -9
- package/docs/troubleshooting.md +13 -1
- package/docs/usage.md +18 -3
- package/lib/client/OutlinePanel.js +104 -19
- package/lib/client/outline.js +11 -2
- package/lib/client/styles.js +24 -0
- package/lib/client.js +125 -27
- package/lib/client.js.map +1 -1
- package/lib/types/client/outline.d.ts +8 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@chestnut23/dsh-conversation-outline)
|
|
6
|
+
[](https://www.npmjs.com/package/@chestnut23/dsh-conversation-outline)
|
|
7
|
+
[](LICENSE)
|
|
7
8
|
[](https://nodejs.org/)
|
|
8
9
|
[](https://github.com/lzbaclz/dsh-conversation-outline)
|
|
9
10
|
[](https://github.com/deepseek-ai/DeepSeek-Harness)
|
|
@@ -25,10 +26,13 @@ a flash highlight so you cannot miss it. Bilingual zh-CN / en.
|
|
|
25
26
|
chronological order. Out of the way while you read; >60 questions fold into a `+N` marker.
|
|
26
27
|
- **Hover to preview** — hover the rail and the panel slides out with each question's
|
|
27
28
|
opening words (single-line truncated), `#turn` badge and `HH:MM` time. Collapses 240 ms
|
|
28
|
-
after you move away
|
|
29
|
-
|
|
29
|
+
after you move away. Pure overlay — your content never shifts.
|
|
30
|
+
- **Click to open as well** — clicking the strip (or a bar) pins the panel open until you
|
|
31
|
+
close it with `Esc`, the ×, or a click outside, so the rail also works on touch, pen and
|
|
32
|
+
automation where no hover event ever arrives.
|
|
30
33
|
- **Click-to-jump** — switches to the Chat view (even from Trajectory), scrolls to the
|
|
31
|
-
message and flashes it for 1.8 s. Respects `prefers-reduced-motion
|
|
34
|
+
message and flashes it for 1.8 s. Respects `prefers-reduced-motion`; a jump that cannot
|
|
35
|
+
reach the message reports it in the panel instead of failing silently.
|
|
32
36
|
- **Search & load older** — case-insensitive filtering, plus paging into older history.
|
|
33
37
|
- **Live** — new questions appear while the session runs; follows the current session and
|
|
34
38
|
collapses on switch.
|
|
@@ -72,14 +76,15 @@ dsh plugin --profile web list
|
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
Upgrade with the same `add` command (optionally pin a version:
|
|
75
|
-
`@chestnut23/dsh-conversation-outline@0.1.
|
|
79
|
+
`@chestnut23/dsh-conversation-outline@0.1.2`).
|
|
76
80
|
|
|
77
81
|
## Usage
|
|
78
82
|
|
|
79
83
|
Install, restart, open any session that already has messages — a thin strip appears on
|
|
80
|
-
the right edge. Hover it to preview questions
|
|
81
|
-
filter, `Load older` to page
|
|
82
|
-
[docs/usage.md](docs/usage.md). Something's not
|
|
84
|
+
the right edge. Hover it to preview questions; click the strip (or a bar) to open the
|
|
85
|
+
panel and keep it open; click a row to jump; search to filter, `Load older` to page
|
|
86
|
+
through history. Full walkthrough: [docs/usage.md](docs/usage.md). Something's not
|
|
87
|
+
showing up? [Troubleshooting](docs/troubleshooting.md).
|
|
83
88
|
|
|
84
89
|
## Preview
|
|
85
90
|
|
package/README.zh.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@chestnut23/dsh-conversation-outline)
|
|
6
|
+
[](https://www.npmjs.com/package/@chestnut23/dsh-conversation-outline)
|
|
7
|
+
[](LICENSE)
|
|
7
8
|
[](https://nodejs.org/)
|
|
8
9
|
[](https://github.com/lzbaclz/dsh-conversation-outline)
|
|
9
10
|
[](https://github.com/deepseek-ai/DeepSeek-Harness)
|
|
@@ -23,10 +24,13 @@
|
|
|
23
24
|
- **右侧细条(minimap)**——紧贴右边缘的窄竖条,按时间顺序每个问题一根小横条。
|
|
24
25
|
平时不占地方、不影响阅读;超过 60 个问题折叠为顶部 `+N` 标记。
|
|
25
26
|
- **悬停预览**——鼠标移到细条上,面板滑出:每个问题开头几个字(单行截断)、`#轮次`
|
|
26
|
-
徽标与 `HH:MM` 时间。移开 240ms
|
|
27
|
-
|
|
27
|
+
徽标与 `HH:MM` 时间。移开 240ms 后自动收起。纯覆盖层,**正文布局纹丝不动**。
|
|
28
|
+
- **点击也能打开**——点细条本身(或任意一根小横条)会把面板**钉住**,直到你用 `Esc`、
|
|
29
|
+
× 或点击面板外才关闭;因此触屏、手写笔、自动化脚本这类「永远没有 hover 事件」的场景
|
|
30
|
+
同样可用。
|
|
28
31
|
- **点击跳转**——即使停在「轨迹」视图也会自动切回聊天视图,滚动定位并闪烁 1.8s,
|
|
29
|
-
尊重 `prefers-reduced-motion
|
|
32
|
+
尊重 `prefers-reduced-motion`;万一目标消息不在已加载的历史窗口里,面板会明确告诉你,
|
|
33
|
+
而不是静默失败。
|
|
30
34
|
- **搜索 & 加载更早**——大小写不敏感过滤 + 翻历史分页。
|
|
31
35
|
- **实时**——会话进行中新问题自动出现;跟随当前会话,切换即收起。
|
|
32
36
|
- **运行时零依赖**——浏览器包只引用 React 等平台公共模块,其余全部内联并在构建期
|
|
@@ -67,13 +71,13 @@ dsh plugin --profile web add "link:$(pwd)"
|
|
|
67
71
|
dsh plugin --profile web list
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
升级用同一条 `add` 命令(可钉版本:`@chestnut23/dsh-conversation-outline@0.1.
|
|
74
|
+
升级用同一条 `add` 命令(可钉版本:`@chestnut23/dsh-conversation-outline@0.1.2`)。
|
|
71
75
|
|
|
72
76
|
## 使用
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
装好重启后,打开任何一个已有消息的会话,右侧边缘会出现细竖条:悬停预览问题;**点击细条
|
|
79
|
+
(或小横条)打开并钉住面板**;点列表行跳转;搜索过滤、`加载更早` 翻历史。
|
|
80
|
+
完整走查:[docs/usage.md](docs/usage.md)。没看到?先看 [常见问题](docs/troubleshooting.md)。
|
|
77
81
|
|
|
78
82
|
## 界面预览
|
|
79
83
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -14,10 +14,22 @@
|
|
|
14
14
|
Your page is still running an old bundle. Refresh the page (Ctrl/Cmd+R). If a stale
|
|
15
15
|
version persists, restart the DSH Web service and refresh again.
|
|
16
16
|
|
|
17
|
+
## The rail shows but the panel will not open
|
|
18
|
+
|
|
19
|
+
Click the strip (or any bar) — since v0.1.1 a click pins the panel open, so it no longer
|
|
20
|
+
depends on a hover event reaching the rail. If a click still does nothing at all:
|
|
21
|
+
|
|
22
|
+
1. Check the pointer is on the strip, not on the window edge outside the app frame.
|
|
23
|
+
2. Open the browser console: a failure inside the jump path logs the target key.
|
|
24
|
+
3. Refresh the page once — a bundle in the page cache can predate the fix.
|
|
25
|
+
4. If it still fails, report the console output with the DSH version and the plugin
|
|
26
|
+
version (`dsh plugin --profile web list`).
|
|
27
|
+
|
|
17
28
|
## Jumping does nothing / logs "jump failed"
|
|
18
29
|
|
|
19
30
|
- The target message must be in the **loaded history window** (the panel only lists
|
|
20
|
-
loaded questions). Click `Load older` first for deeper history, then jump again.
|
|
31
|
+
loaded questions). Click `Load older` first for deeper history, then jump again. The
|
|
32
|
+
panel now shows this notice itself instead of failing silently.
|
|
21
33
|
- Extremely long sessions: if the row does not render within 1.5 s after switching
|
|
22
34
|
views, the jump is abandoned (a console warning is logged). Retrying usually works.
|
|
23
35
|
|
package/docs/usage.md
CHANGED
|
@@ -19,8 +19,21 @@ A thin vertical strip appears on the right edge, vertically centered:
|
|
|
19
19
|
time. Mid-turn steering messages carry a `追问` / `steer` tag.
|
|
20
20
|
- Move the pointer away: the panel collapses after a 240 ms grace period (the grace
|
|
21
21
|
lets the pointer travel from the rail into the panel without flicker).
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
## Open by click (no hover required)
|
|
24
|
+
|
|
25
|
+
Click the rail itself — its padding, its background, or the `+N` marker — and the panel
|
|
26
|
+
opens **pinned**: it stays up until it is closed, so the strip is fully usable by click
|
|
27
|
+
alone (touch, pen, automation, or a pointer that never raised a hover event).
|
|
28
|
+
|
|
29
|
+
- Clicking a bar both opens the panel and jumps to that question's message; the panel
|
|
30
|
+
closes again once the message is on screen.
|
|
31
|
+
- A pinned panel keeps a highlighted border, so "pinned" and "hover preview" look
|
|
32
|
+
different.
|
|
33
|
+
- Closing it: `Esc`, the × button, a click anywhere outside the panel, or clicking the
|
|
34
|
+
strip again.
|
|
35
|
+
- If a jump cannot find the message (it sits outside the loaded history window), the
|
|
36
|
+
panel says so instead of failing silently — click `Load older`, then jump again.
|
|
24
37
|
|
|
25
38
|
## Jump to a message
|
|
26
39
|
|
|
@@ -30,11 +43,13 @@ Click a bar on the rail — or a row in the panel:
|
|
|
30
43
|
view first.
|
|
31
44
|
2. The chat scrolls so the target message sits ~96px below the top.
|
|
32
45
|
3. The message flashes for 1.8 s.
|
|
46
|
+
4. The panel closes once the jump landed.
|
|
33
47
|
|
|
34
48
|
Notes:
|
|
35
49
|
|
|
36
50
|
- Jumping works only for messages already inside the loaded history window (the panel
|
|
37
|
-
only lists loaded questions — use `Load older` first for deeper history).
|
|
51
|
+
only lists loaded questions — use `Load older` first for deeper history). A failed
|
|
52
|
+
jump is reported in the panel footer.
|
|
38
53
|
- `prefers-reduced-motion` disables the smooth scroll and the flash animation.
|
|
39
54
|
|
|
40
55
|
## Search
|
|
@@ -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
|
-
|
|
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
|
-
|
|
105
|
+
setPinned(false);
|
|
106
|
+
setHovered(false);
|
|
107
|
+
setJumpFailed(null);
|
|
86
108
|
}, [current, clearPending]);
|
|
87
|
-
// Escape closes the panel (hover-opened or
|
|
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
|
-
|
|
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,27 +141,59 @@ 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
|
-
// ----
|
|
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
|
-
|
|
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
|
-
|
|
168
|
+
/** Pointer entered the rail or the panel: cancel a pending collapse, preview. */
|
|
169
|
+
const onEnter = useCallback(() => {
|
|
121
170
|
cancelCollapse();
|
|
122
|
-
|
|
171
|
+
setHovered(true);
|
|
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);
|
|
123
189
|
}, [cancelCollapse]);
|
|
124
190
|
/** Find the chat row for a node key (pure predicate over DOM rows). */
|
|
125
191
|
const findRow = useCallback((key) => {
|
|
126
|
-
|
|
192
|
+
// Exact match on either anchor attribute the chat flow renders for a node:
|
|
193
|
+
// `data-chat-anchor-key` is the one the platform's own anchorElement() uses,
|
|
194
|
+
// `data-chat-flow-key` is its sibling on the same wrapper. No guessing at
|
|
195
|
+
// the key's internals — a wrong row is worse than a reported failure.
|
|
196
|
+
const rows = document.querySelectorAll('[data-chat-anchor-key], [data-chat-flow-key]');
|
|
127
197
|
for (const row of rows) {
|
|
128
198
|
if (isJumpTargetRow(row, key))
|
|
129
199
|
return row;
|
|
@@ -155,11 +225,12 @@ export function OutlinePanel({ sessions, useSessions, t }) {
|
|
|
155
225
|
}, FLASH_MS);
|
|
156
226
|
}, []);
|
|
157
227
|
/** Jump-to-message algorithm (spec §2.2). */
|
|
158
|
-
const jumpTo = useCallback((key) => {
|
|
228
|
+
const jumpTo = useCallback((key, label) => {
|
|
159
229
|
// The conversation page must be mounted; abort silently otherwise.
|
|
160
230
|
const scrollport = document.querySelector('[data-conversation-scroll]');
|
|
161
231
|
if (!scrollport)
|
|
162
232
|
return;
|
|
233
|
+
setJumpFailed(null);
|
|
163
234
|
// Ensure the Chat view is active: chat is order 0 — the FIRST tab of the
|
|
164
235
|
// conversation-root tablist, when one exists (setView is idempotent).
|
|
165
236
|
// A profile without extra views has no header tablist at all — that is
|
|
@@ -178,18 +249,21 @@ export function OutlinePanel({ sessions, useSessions, t }) {
|
|
|
178
249
|
const row = findRow(key);
|
|
179
250
|
if (row) {
|
|
180
251
|
flashAndScroll(row);
|
|
181
|
-
|
|
252
|
+
closePanel();
|
|
182
253
|
return;
|
|
183
254
|
}
|
|
184
255
|
if (Date.now() < deadline) {
|
|
185
256
|
scheduleRaf(rafsRef, poll);
|
|
186
257
|
}
|
|
187
258
|
else {
|
|
259
|
+
// Never fail silently: the panel states what happened, and the
|
|
260
|
+
// console keeps the key for bug reports.
|
|
188
261
|
console.warn(t('jumpFailed'), { key });
|
|
262
|
+
setJumpFailed(label ?? key);
|
|
189
263
|
}
|
|
190
264
|
};
|
|
191
265
|
scheduleRaf(rafsRef, poll);
|
|
192
|
-
}, [findRow, flashAndScroll, t]);
|
|
266
|
+
}, [findRow, flashAndScroll, t, closePanel]);
|
|
193
267
|
/** Copy a question's text (clipboard API with execCommand fallback). */
|
|
194
268
|
const copyText = useCallback((text, key) => {
|
|
195
269
|
const done = () => {
|
|
@@ -231,16 +305,27 @@ export function OutlinePanel({ sessions, useSessions, t }) {
|
|
|
231
305
|
const start = Math.max(0, allItems.length - MAX_BARS);
|
|
232
306
|
const bars = allItems.slice(start);
|
|
233
307
|
const overflow = start;
|
|
234
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "dso-rail", role: "group", "aria-label": t('title'), tabIndex: 0, onMouseEnter:
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
308
|
+
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) => {
|
|
309
|
+
// Any click on the strip pins the panel open — the container itself,
|
|
310
|
+
// its padding, or the "+N" marker. Bars stop propagation and reach
|
|
311
|
+
// the same pin through the shared jump gesture below.
|
|
312
|
+
if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) {
|
|
313
|
+
togglePin();
|
|
314
|
+
}
|
|
238
315
|
}, onKeyDown: (event) => {
|
|
239
316
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
240
317
|
event.preventDefault();
|
|
241
|
-
|
|
318
|
+
togglePin();
|
|
242
319
|
}
|
|
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: (
|
|
320
|
+
}, "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) => {
|
|
321
|
+
event.stopPropagation();
|
|
322
|
+
// The bar's own job is the jump; the panel comes up pinned first
|
|
323
|
+
// so a click always has a visible effect, then jumpTo collapses
|
|
324
|
+
// it once the row is on screen.
|
|
325
|
+
setPinned(true);
|
|
326
|
+
setHovered(true);
|
|
327
|
+
jumpTo(item.key, item.text);
|
|
328
|
+
} }, 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
329
|
void session.loadOlder();
|
|
245
330
|
}, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));
|
|
246
331
|
}
|
package/lib/client/outline.js
CHANGED
|
@@ -77,7 +77,16 @@ export function formatTime(ms) {
|
|
|
77
77
|
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
78
78
|
return `${hh}:${mm}`;
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
80
|
+
/** Attributes the chat flow renders a node's identity under. */
|
|
81
|
+
const JUMP_KEY_ATTRIBUTES = ['data-chat-anchor-key', 'data-chat-flow-key'];
|
|
82
|
+
/**
|
|
83
|
+
* Pure DOM predicate for the jump loop: is this row the target node?
|
|
84
|
+
*
|
|
85
|
+
* Exact match only, against either attribute the flow wrapper carries — the
|
|
86
|
+
* platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling
|
|
87
|
+
* `data-chat-flow-key` holds the same value, so a row is still found when only
|
|
88
|
+
* one of the two is present.
|
|
89
|
+
*/
|
|
81
90
|
export function isJumpTargetRow(row, key) {
|
|
82
|
-
return row.getAttribute(
|
|
91
|
+
return JUMP_KEY_ATTRIBUTES.some((attribute) => row.getAttribute(attribute) === key);
|
|
83
92
|
}
|
package/lib/client/styles.js
CHANGED
|
@@ -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
|
@@ -71,9 +71,18 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
const d = new Date(ms);
|
|
72
72
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/** Attributes the chat flow renders a node's identity under. */
|
|
75
|
+
const JUMP_KEY_ATTRIBUTES = ["data-chat-anchor-key", "data-chat-flow-key"];
|
|
76
|
+
/**
|
|
77
|
+
* Pure DOM predicate for the jump loop: is this row the target node?
|
|
78
|
+
*
|
|
79
|
+
* Exact match only, against either attribute the flow wrapper carries — the
|
|
80
|
+
* platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling
|
|
81
|
+
* `data-chat-flow-key` holds the same value, so a row is still found when only
|
|
82
|
+
* one of the two is present.
|
|
83
|
+
*/
|
|
75
84
|
function isJumpTargetRow(row, key) {
|
|
76
|
-
return row.getAttribute(
|
|
85
|
+
return JUMP_KEY_ATTRIBUTES.some((attribute) => row.getAttribute(attribute) === key);
|
|
77
86
|
}
|
|
78
87
|
//#endregion
|
|
79
88
|
//#region lib/client/OutlinePanel.js
|
|
@@ -124,13 +133,18 @@ window.__ModuleLoader__.load({
|
|
|
124
133
|
const current = useSessions((s) => s.current);
|
|
125
134
|
const session = current ? sessions.binding(current)?.session : void 0;
|
|
126
135
|
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 [
|
|
136
|
+
const [pinned, setPinned] = (0, react.useState)(false);
|
|
137
|
+
const [hovered, setHovered] = (0, react.useState)(false);
|
|
138
|
+
const open = pinned || hovered;
|
|
128
139
|
const [query, setQuery] = (0, react.useState)("");
|
|
129
140
|
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
141
|
+
const [jumpFailed, setJumpFailed] = (0, react.useState)(null);
|
|
130
142
|
const timeoutsRef = (0, react.useRef)([]);
|
|
131
143
|
const rafsRef = (0, react.useRef)([]);
|
|
132
144
|
const flashedRowRef = (0, react.useRef)(null);
|
|
133
145
|
const collapseTimerRef = (0, react.useRef)(null);
|
|
146
|
+
const railRef = (0, react.useRef)(null);
|
|
147
|
+
const panelRef = (0, react.useRef)(null);
|
|
134
148
|
const clearPending = (0, react.useCallback)(() => {
|
|
135
149
|
for (const id of rafsRef.current) window.cancelAnimationFrame(id);
|
|
136
150
|
for (const id of timeoutsRef.current) window.clearTimeout(id);
|
|
@@ -145,18 +159,37 @@ window.__ModuleLoader__.load({
|
|
|
145
159
|
flashedRowRef.current = null;
|
|
146
160
|
}
|
|
147
161
|
}, []);
|
|
162
|
+
/** Close the panel from any trigger: drop the pin and the hover preview. */
|
|
163
|
+
const closePanel = (0, react.useCallback)(() => {
|
|
164
|
+
setPinned(false);
|
|
165
|
+
setHovered(false);
|
|
166
|
+
cancelCollapseRef.current();
|
|
167
|
+
}, []);
|
|
148
168
|
(0, react.useEffect)(() => {
|
|
149
169
|
clearPending();
|
|
150
|
-
|
|
170
|
+
setPinned(false);
|
|
171
|
+
setHovered(false);
|
|
172
|
+
setJumpFailed(null);
|
|
151
173
|
}, [current, clearPending]);
|
|
152
174
|
(0, react.useEffect)(() => {
|
|
153
175
|
if (!open) return;
|
|
154
176
|
const onKey = (event) => {
|
|
155
|
-
if (event.key === "Escape")
|
|
177
|
+
if (event.key === "Escape") closePanel();
|
|
156
178
|
};
|
|
157
179
|
window.addEventListener("keydown", onKey);
|
|
158
180
|
return () => window.removeEventListener("keydown", onKey);
|
|
159
|
-
}, [open]);
|
|
181
|
+
}, [open, closePanel]);
|
|
182
|
+
(0, react.useEffect)(() => {
|
|
183
|
+
if (!pinned) return;
|
|
184
|
+
const onPointerDown = (event) => {
|
|
185
|
+
const target = event.target;
|
|
186
|
+
if (!(target instanceof Node)) return;
|
|
187
|
+
if (railRef.current?.contains(target) || panelRef.current?.contains(target)) return;
|
|
188
|
+
closePanel();
|
|
189
|
+
};
|
|
190
|
+
window.addEventListener("mousedown", onPointerDown);
|
|
191
|
+
return () => window.removeEventListener("mousedown", onPointerDown);
|
|
192
|
+
}, [pinned, closePanel]);
|
|
160
193
|
(0, react.useEffect)(() => clearPending, [clearPending]);
|
|
161
194
|
const allItems = (0, react.useMemo)(() => snapshot ? collectQuestions(snapshot) : [], [snapshot]);
|
|
162
195
|
const items = (0, react.useMemo)(() => filterQuestions(allItems, query), [allItems, query]);
|
|
@@ -169,20 +202,40 @@ window.__ModuleLoader__.load({
|
|
|
169
202
|
collapseTimerRef.current = null;
|
|
170
203
|
}
|
|
171
204
|
}, []);
|
|
205
|
+
const cancelCollapseRef = (0, react.useRef)(cancelCollapse);
|
|
206
|
+
cancelCollapseRef.current = cancelCollapse;
|
|
172
207
|
const scheduleCollapse = (0, react.useCallback)(() => {
|
|
173
208
|
cancelCollapse();
|
|
174
209
|
collapseTimerRef.current = window.setTimeout(() => {
|
|
175
210
|
collapseTimerRef.current = null;
|
|
176
|
-
|
|
211
|
+
if (railRef.current?.matches(":hover") || panelRef.current?.matches(":hover")) return;
|
|
212
|
+
setHovered(false);
|
|
177
213
|
}, COLLAPSE_DELAY_MS);
|
|
178
214
|
}, [cancelCollapse]);
|
|
179
|
-
|
|
215
|
+
/** Pointer entered the rail or the panel: cancel a pending collapse, preview. */
|
|
216
|
+
const onEnter = (0, react.useCallback)(() => {
|
|
180
217
|
cancelCollapse();
|
|
181
|
-
|
|
218
|
+
setHovered(true);
|
|
219
|
+
}, [cancelCollapse]);
|
|
220
|
+
/** Pointer left: let the grace period decide, unless the panel is pinned. */
|
|
221
|
+
const onLeave = (0, react.useCallback)(() => {
|
|
222
|
+
if (pinned) return;
|
|
223
|
+
scheduleCollapse();
|
|
224
|
+
}, [pinned, scheduleCollapse]);
|
|
225
|
+
/** A click on the rail (or any bar) opens the panel and keeps it open. */
|
|
226
|
+
const togglePin = (0, react.useCallback)(() => {
|
|
227
|
+
cancelCollapse();
|
|
228
|
+
setPinned((value) => !value);
|
|
229
|
+
setHovered(true);
|
|
230
|
+
}, [cancelCollapse]);
|
|
231
|
+
/** The panel's own controls: clicking inside never closes it. */
|
|
232
|
+
const keepOpen = (0, react.useCallback)(() => {
|
|
233
|
+
cancelCollapse();
|
|
234
|
+
setHovered(true);
|
|
182
235
|
}, [cancelCollapse]);
|
|
183
236
|
/** Find the chat row for a node key (pure predicate over DOM rows). */
|
|
184
237
|
const findRow = (0, react.useCallback)((key) => {
|
|
185
|
-
const rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
238
|
+
const rows = document.querySelectorAll("[data-chat-anchor-key], [data-chat-flow-key]");
|
|
186
239
|
for (const row of rows) if (isJumpTargetRow(row, key)) return row;
|
|
187
240
|
return null;
|
|
188
241
|
}, []);
|
|
@@ -208,9 +261,10 @@ window.__ModuleLoader__.load({
|
|
|
208
261
|
}, FLASH_MS);
|
|
209
262
|
}, []);
|
|
210
263
|
/** Jump-to-message algorithm (spec §2.2). */
|
|
211
|
-
const jumpTo = (0, react.useCallback)((key) => {
|
|
264
|
+
const jumpTo = (0, react.useCallback)((key, label) => {
|
|
212
265
|
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
213
266
|
if (!scrollport) return;
|
|
267
|
+
setJumpFailed(null);
|
|
214
268
|
const tablist = findConversationTablist(scrollport);
|
|
215
269
|
if (tablist) {
|
|
216
270
|
const firstTab = tablist.querySelector("button[role=\"tab\"]");
|
|
@@ -221,17 +275,21 @@ window.__ModuleLoader__.load({
|
|
|
221
275
|
const row = findRow(key);
|
|
222
276
|
if (row) {
|
|
223
277
|
flashAndScroll(row);
|
|
224
|
-
|
|
278
|
+
closePanel();
|
|
225
279
|
return;
|
|
226
280
|
}
|
|
227
281
|
if (Date.now() < deadline) scheduleRaf(rafsRef, poll);
|
|
228
|
-
else
|
|
282
|
+
else {
|
|
283
|
+
console.warn(t("jumpFailed"), { key });
|
|
284
|
+
setJumpFailed(label ?? key);
|
|
285
|
+
}
|
|
229
286
|
};
|
|
230
287
|
scheduleRaf(rafsRef, poll);
|
|
231
288
|
}, [
|
|
232
289
|
findRow,
|
|
233
290
|
flashAndScroll,
|
|
234
|
-
t
|
|
291
|
+
t,
|
|
292
|
+
closePanel
|
|
235
293
|
]);
|
|
236
294
|
/** Copy a question's text (clipboard API with execCommand fallback). */
|
|
237
295
|
const copyText = (0, react.useCallback)((text, key) => {
|
|
@@ -261,23 +319,25 @@ window.__ModuleLoader__.load({
|
|
|
261
319
|
const bars = allItems.slice(start);
|
|
262
320
|
const overflow = start;
|
|
263
321
|
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
322
|
+
ref: railRef,
|
|
264
323
|
className: "dso-rail",
|
|
265
324
|
role: "group",
|
|
266
325
|
"aria-label": t("title"),
|
|
267
326
|
tabIndex: 0,
|
|
268
|
-
onMouseEnter:
|
|
269
|
-
onMouseLeave:
|
|
270
|
-
onFocus:
|
|
271
|
-
onBlur:
|
|
327
|
+
onMouseEnter: onEnter,
|
|
328
|
+
onMouseLeave: onLeave,
|
|
329
|
+
onFocus: onEnter,
|
|
330
|
+
onBlur: onLeave,
|
|
272
331
|
onClick: (event) => {
|
|
273
|
-
if (event.target === event.currentTarget)
|
|
332
|
+
if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) togglePin();
|
|
274
333
|
},
|
|
275
334
|
onKeyDown: (event) => {
|
|
276
335
|
if (event.key === "Enter" || event.key === " ") {
|
|
277
336
|
event.preventDefault();
|
|
278
|
-
|
|
337
|
+
togglePin();
|
|
279
338
|
}
|
|
280
339
|
},
|
|
340
|
+
"data-dso-open": open ? "true" : void 0,
|
|
281
341
|
children: [overflow > 0 && (0, react_jsx_runtime.jsxs)("span", {
|
|
282
342
|
className: "dso-rail-more",
|
|
283
343
|
"aria-label": t("moreBars", { count: overflow }),
|
|
@@ -286,16 +346,24 @@ window.__ModuleLoader__.load({
|
|
|
286
346
|
type: "button",
|
|
287
347
|
className: "dso-bar",
|
|
288
348
|
"aria-label": t("barLabel", { n: start + index + 1 }),
|
|
289
|
-
onClick: () =>
|
|
349
|
+
onClick: (event) => {
|
|
350
|
+
event.stopPropagation();
|
|
351
|
+
setPinned(true);
|
|
352
|
+
setHovered(true);
|
|
353
|
+
jumpTo(item.key, item.text);
|
|
354
|
+
}
|
|
290
355
|
}, item.key))]
|
|
291
356
|
}), open && (0, react_jsx_runtime.jsxs)("div", {
|
|
357
|
+
ref: panelRef,
|
|
292
358
|
className: "dso-panel",
|
|
293
359
|
role: "region",
|
|
294
360
|
"aria-label": t("title"),
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
361
|
+
"data-dso-pinned": pinned ? "true" : void 0,
|
|
362
|
+
onMouseEnter: onEnter,
|
|
363
|
+
onMouseLeave: onLeave,
|
|
364
|
+
onFocus: onEnter,
|
|
365
|
+
onBlur: onLeave,
|
|
366
|
+
onMouseDown: keepOpen,
|
|
299
367
|
children: [
|
|
300
368
|
(0, react_jsx_runtime.jsxs)("header", {
|
|
301
369
|
className: "dso-panel-header",
|
|
@@ -312,7 +380,7 @@ window.__ModuleLoader__.load({
|
|
|
312
380
|
type: "button",
|
|
313
381
|
className: "dso-panel-close",
|
|
314
382
|
"aria-label": t("close"),
|
|
315
|
-
onClick:
|
|
383
|
+
onClick: closePanel,
|
|
316
384
|
children: "×"
|
|
317
385
|
})
|
|
318
386
|
]
|
|
@@ -335,7 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
335
403
|
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
336
404
|
type: "button",
|
|
337
405
|
className: "dso-row-main",
|
|
338
|
-
onClick: () => jumpTo(item.key),
|
|
406
|
+
onClick: () => jumpTo(item.key, item.text),
|
|
339
407
|
children: [
|
|
340
408
|
(0, react_jsx_runtime.jsx)("span", {
|
|
341
409
|
className: "dso-turn",
|
|
@@ -363,6 +431,12 @@ window.__ModuleLoader__.load({
|
|
|
363
431
|
})]
|
|
364
432
|
}, item.key))
|
|
365
433
|
}),
|
|
434
|
+
jumpFailed !== null && (0, react_jsx_runtime.jsx)("p", {
|
|
435
|
+
className: "dso-jump-failed",
|
|
436
|
+
role: "status",
|
|
437
|
+
"data-dso-jump-failed": "true",
|
|
438
|
+
children: t("jumpFailed")
|
|
439
|
+
}),
|
|
366
440
|
hasMore && (0, react_jsx_runtime.jsx)("footer", {
|
|
367
441
|
className: "dso-footer",
|
|
368
442
|
children: (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -472,6 +546,15 @@ window.__ModuleLoader__.load({
|
|
|
472
546
|
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
473
547
|
outline-offset: 2px;
|
|
474
548
|
}
|
|
549
|
+
/* Clickability affordance: a hovered or pinned strip lights its bars up, so
|
|
550
|
+
it reads as a control rather than as decoration. */
|
|
551
|
+
.dso-rail:hover .dso-bar,
|
|
552
|
+
.dso-rail[data-dso-open] .dso-bar {
|
|
553
|
+
background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);
|
|
554
|
+
}
|
|
555
|
+
.dso-rail:hover .dso-bar:hover {
|
|
556
|
+
background: var(--dsw-alias-state-business-primary);
|
|
557
|
+
}
|
|
475
558
|
|
|
476
559
|
/* ---- Hover-expanded preview panel --------------------------------------- */
|
|
477
560
|
.dso-panel {
|
|
@@ -494,6 +577,11 @@ window.__ModuleLoader__.load({
|
|
|
494
577
|
color: var(--dsw-alias-label-primary);
|
|
495
578
|
animation: dso-panel-in 140ms ease-out;
|
|
496
579
|
}
|
|
580
|
+
/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it
|
|
581
|
+
no longer collapses when the pointer leaves. */
|
|
582
|
+
.dso-panel[data-dso-pinned] {
|
|
583
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
584
|
+
}
|
|
497
585
|
@keyframes dso-panel-in {
|
|
498
586
|
from { opacity: 0; transform: translateY(-50%) translateX(12px); }
|
|
499
587
|
to { opacity: 1; transform: translateY(-50%) translateX(0); }
|
|
@@ -662,6 +750,16 @@ window.__ModuleLoader__.load({
|
|
|
662
750
|
color: var(--dsw-alias-label-tertiary);
|
|
663
751
|
}
|
|
664
752
|
|
|
753
|
+
/* ---- Jump failure notice ------------------------------------------------ */
|
|
754
|
+
.dso-jump-failed {
|
|
755
|
+
margin: 0;
|
|
756
|
+
padding: 8px 14px;
|
|
757
|
+
border-top: 1px solid var(--dsw-alias-line-normal);
|
|
758
|
+
font-size: 12px;
|
|
759
|
+
line-height: 1.5;
|
|
760
|
+
color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));
|
|
761
|
+
}
|
|
762
|
+
|
|
665
763
|
/* ---- Footer / load older ------------------------------------------------ */
|
|
666
764
|
.dso-footer {
|
|
667
765
|
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/** Attributes the chat flow renders a node's identity under. */\nconst JUMP_KEY_ATTRIBUTES = ['data-chat-anchor-key', 'data-chat-flow-key'];\n/**\n * Pure DOM predicate for the jump loop: is this row the target node?\n *\n * Exact match only, against either attribute the flow wrapper carries — the\n * platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling\n * `data-chat-flow-key` holds the same value, so a row is still found when only\n * one of the two is present.\n */\nexport function isJumpTargetRow(row, key) {\n return JUMP_KEY_ATTRIBUTES.some((attribute) => row.getAttribute(attribute) === 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 /** Find the chat row for a node key (pure predicate over DOM rows). */\n const findRow = useCallback((key) => {\n // Exact match on either anchor attribute the chat flow renders for a node:\n // `data-chat-anchor-key` is the one the platform's own anchorElement() uses,\n // `data-chat-flow-key` is its sibling on the same wrapper. No guessing at\n // the key's internals — a wrong row is worse than a reported failure.\n const rows = document.querySelectorAll('[data-chat-anchor-key], [data-chat-flow-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, 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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"}
|
|
@@ -78,5 +78,12 @@ export declare function formatTime(ms: number): string;
|
|
|
78
78
|
export interface ElementLike {
|
|
79
79
|
getAttribute(name: string): string | null;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Pure DOM predicate for the jump loop: is this row the target node?
|
|
83
|
+
*
|
|
84
|
+
* Exact match only, against either attribute the flow wrapper carries — the
|
|
85
|
+
* platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling
|
|
86
|
+
* `data-chat-flow-key` holds the same value, so a row is still found when only
|
|
87
|
+
* one of the two is present.
|
|
88
|
+
*/
|
|
82
89
|
export declare function isJumpTargetRow(row: ElementLike, key: string): boolean;
|
|
@@ -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.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|