dsh-quick-toc 0.7.14 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +16 -0
- package/CHANGELOG.md +16 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +414 -49
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.8.1] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **The pin on the hover preview could not be clicked**: moving from an outline row towards the card started the fade first, so the card was gone before the pin was reached. Leaving a row now only arms the close, and reaching the card cancels it.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **The collapsed handle took room from the text in a narrow window**: below a 760-pixel conversation column the collapsed side handle slides back into the divider; the droplet above stays as the way into the outline, and the side handle returns when the window widens.
|
|
12
|
+
|
|
13
|
+
## [0.8.0] - 2026-09-29
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The hover preview can be pinned**: a pin button on the card keeps it on screen after the pointer moves away, and its text can be selected; click the pin again, press Escape or click elsewhere to drop it. One pinned card at a time, and it stays where it was put.
|
|
17
|
+
- **Two previews side by side**: while a card is pinned, hovering another row opens a second card next to it — on whichever side has more room — so two places can be compared.
|
|
18
|
+
- **A context menu on outline rows**: right-click a heading row for "Pin this preview / Unpin" and "Jump to the end of this section".
|
|
19
|
+
- **Cards and menus animate in and out**: the pinned card, the hover preview and the row menu all fade in and out instead of appearing or vanishing instantly.
|
|
20
|
+
|
|
5
21
|
## [0.7.14] - 2026-09-28
|
|
6
22
|
|
|
7
23
|
### Changed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.8.1] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **悬停预览卡上的图钉无法点击**:鼠标从大纲行移向卡片时会先开始淡出,卡片在图钉被点到之前就已经消失。现在离开行只是预备关闭,指针落到卡片上即取消。
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
- **窄窗口下折叠把手占用正文**:对话区域宽度不足 760 像素时,折叠态的侧边把手滑回分隔线内;顶部的水滴把手保留,作为打开大纲的入口,窗口变宽后侧边把手自动回来。
|
|
12
|
+
|
|
13
|
+
## [0.8.0] - 2026-09-29
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
- **悬停预览可以钉住**:预览卡右上角多了一个图钉,点一下把它钉在屏幕上——鼠标移开也不消失,文字可以选中;再点一次图钉、按 Esc 或点别处取消。同时只保留一张钉住的卡,钉住的位置不会再动。
|
|
17
|
+
- **钉住后可以两处对照**:钉住状态下悬停别的行会同时显示第二张卡,两张不重叠——第二张放在空得多的一侧。
|
|
18
|
+
- **大纲行右键菜单**:在标题行上点右键可以「钉住预览/取消钉住」或「跳到本节末尾」。
|
|
19
|
+
- **卡片与菜单的收放都有动画**:钉住的卡、悬停预览和右键菜单都以淡入淡出出现和消失,不再瞬间弹出或消失。
|
|
20
|
+
|
|
5
21
|
## [0.7.14] - 2026-09-28
|
|
6
22
|
|
|
7
23
|
### 变更
|
package/README.en.md
CHANGED
|
@@ -42,7 +42,8 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
42
42
|
|
|
43
43
|
| Plugin | Supported DSH |
|
|
44
44
|
| --- | --- |
|
|
45
|
-
| **0.
|
|
45
|
+
| **0.8.x** (latest, 0.8.1) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1 |
|
|
46
|
+
| 0.7.x (0.7.14) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1 |
|
|
46
47
|
| 0.6.x (0.6.3) | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
47
48
|
| 0.5.x (0.5.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
48
49
|
| 0.4.x (0.4.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
|
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
|
|
43
43
|
| 插件版本 | 支持的 DSH 版本 |
|
|
44
44
|
| --- | --- |
|
|
45
|
-
| **0.
|
|
45
|
+
| **0.8.x**(最新,0.8.1) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1 |
|
|
46
|
+
| 0.7.x(0.7.14) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1 |
|
|
46
47
|
| 0.6.x(0.6.3) | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
47
48
|
| 0.5.x(0.5.1) | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
48
49
|
| 0.4.x(0.4.1) | 0.1.5-rc.1、0.1.5-rc.2 |
|
package/lib/client.js
CHANGED
|
@@ -44,6 +44,11 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
var Z_PANEL = 14; // the docked panel
|
|
45
45
|
var Z_SHEET = 16; // the curtain, which replaces the docked panel while it is up
|
|
46
46
|
var Z_HOVER = 18; // hover preview card: above both, still inside the free band
|
|
47
|
+
// A narrow conversation column has no room for a handle floating over the text: below
|
|
48
|
+
// NARROW_CONV_W the collapsed-panel handle slides back into the divider (its usual
|
|
49
|
+
// motion) while the droplet above the conversation stays as the way in. Nothing opens
|
|
50
|
+
// by itself — a resize never overrides the reader's own choice of surface.
|
|
51
|
+
var NARROW_CONV_W = 760;
|
|
47
52
|
var Z_ERROR = 32; // crash banner: above our layers and the app's chrome, below
|
|
48
53
|
// DSH's popovers (100) and modals (1000)
|
|
49
54
|
var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
|
|
@@ -148,6 +153,11 @@ window.__ModuleLoader__.load({
|
|
|
148
153
|
"time.yesterday": "昨天",
|
|
149
154
|
"time.beforeYesterday": "前天",
|
|
150
155
|
"preview.truncated": "预览由宿主提供,可能被截断",
|
|
156
|
+
"preview.pin": "钉住预览",
|
|
157
|
+
"preview.unpin": "取消钉住",
|
|
158
|
+
"menu.pin": "钉住预览",
|
|
159
|
+
"menu.unpin": "取消钉住",
|
|
160
|
+
"menu.end": "跳到本节末尾",
|
|
151
161
|
"resize.w": "拖拽调整宽度",
|
|
152
162
|
"resize.h": "拖拽调整高度",
|
|
153
163
|
"resize.wh": "拖拽同时调整宽高",
|
|
@@ -246,6 +256,11 @@ window.__ModuleLoader__.load({
|
|
|
246
256
|
// carries the numeric date. Yesterday itself reads as the word in both.
|
|
247
257
|
"time.yesterday": "yesterday",
|
|
248
258
|
"preview.truncated": "Preview comes from the host and may be truncated",
|
|
259
|
+
"preview.pin": "Pin this preview",
|
|
260
|
+
"preview.unpin": "Unpin",
|
|
261
|
+
"menu.pin": "Pin this preview",
|
|
262
|
+
"menu.unpin": "Unpin this preview",
|
|
263
|
+
"menu.end": "Jump to the end of this section",
|
|
249
264
|
"resize.w": "Drag to resize the width",
|
|
250
265
|
"resize.h": "Drag to resize the height",
|
|
251
266
|
"resize.wh": "Drag to resize width and height",
|
|
@@ -1564,6 +1579,10 @@ window.__ModuleLoader__.load({
|
|
|
1564
1579
|
var _s2 = react.useState(null);
|
|
1565
1580
|
var viewport = _s2[0];
|
|
1566
1581
|
var setViewport = _s2[1];
|
|
1582
|
+
// how much room the conversation column itself has (the thing a docked panel
|
|
1583
|
+
// squeezes); 0 while it has not been measured yet, which never counts as narrow
|
|
1584
|
+
var convWidth = viewport ? Math.max(0, window.innerWidth - viewport.left - viewport.right) : 0;
|
|
1585
|
+
var narrowWindow = convWidth > 0 && convWidth < NARROW_CONV_W;
|
|
1567
1586
|
react.useEffect(function () {
|
|
1568
1587
|
// Self-healing, because the layout AROUND the conversation can change without
|
|
1569
1588
|
// a resize: the host's own controls move that column about (measured in a real
|
|
@@ -1808,7 +1827,7 @@ window.__ModuleLoader__.load({
|
|
|
1808
1827
|
var scrollToBottom = function (e) {
|
|
1809
1828
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
1810
1829
|
outlineTouchRef.current = Date.now();
|
|
1811
|
-
hoverEnd();
|
|
1830
|
+
hoverEnd(true);
|
|
1812
1831
|
var el = listRef.current;
|
|
1813
1832
|
if (!el) return;
|
|
1814
1833
|
// optimistic: the list is on its way to the newest row, so the button starts
|
|
@@ -1894,7 +1913,7 @@ window.__ModuleLoader__.load({
|
|
|
1894
1913
|
var backToBrowse = function (e) {
|
|
1895
1914
|
if (e && e.stopPropagation) e.stopPropagation();
|
|
1896
1915
|
outlineTouchRef.current = Date.now();
|
|
1897
|
-
hoverEnd();
|
|
1916
|
+
hoverEnd(true);
|
|
1898
1917
|
var el = listRef.current;
|
|
1899
1918
|
var tgt = liveTgt();
|
|
1900
1919
|
if (!el || !tgt) return;
|
|
@@ -1960,6 +1979,17 @@ window.__ModuleLoader__.load({
|
|
|
1960
1979
|
var setHoverClosing = _sHovOut[1];
|
|
1961
1980
|
var hoverTimerRef = react.useRef(null);
|
|
1962
1981
|
var hoverCloseTimerRef = react.useRef(null);
|
|
1982
|
+
// The card carries the pin button, so the pointer has to be able to travel from the
|
|
1983
|
+
// row onto the card without the card fading on the way. Leaving a row therefore only
|
|
1984
|
+
// ARMS the close; the card's own mouseenter disarms it, and the fade starts after a
|
|
1985
|
+
// grace period that a crossing pointer fits into.
|
|
1986
|
+
var HOVER_GRACE_MS = 260;
|
|
1987
|
+
var hoverGraceTimerRef = react.useRef(null);
|
|
1988
|
+
var disarmHoverClose = function () {
|
|
1989
|
+
if (hoverGraceTimerRef.current) { clearTimeout(hoverGraceTimerRef.current); hoverGraceTimerRef.current = null; }
|
|
1990
|
+
if (hoverCloseTimerRef.current) { clearTimeout(hoverCloseTimerRef.current); hoverCloseTimerRef.current = null; }
|
|
1991
|
+
setHoverClosing(false);
|
|
1992
|
+
};
|
|
1963
1993
|
var hoverStart = function (info, el) {
|
|
1964
1994
|
// the hover card is a preference: off means it never opens (the outline rows
|
|
1965
1995
|
// themselves keep their hover tint)
|
|
@@ -1968,11 +1998,7 @@ window.__ModuleLoader__.load({
|
|
|
1968
1998
|
hoverTimerRef.current = setTimeout(function () {
|
|
1969
1999
|
var r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
1970
2000
|
if (!r || r.width <= 0) return;
|
|
1971
|
-
|
|
1972
|
-
clearTimeout(hoverCloseTimerRef.current);
|
|
1973
|
-
hoverCloseTimerRef.current = null;
|
|
1974
|
-
}
|
|
1975
|
-
setHoverClosing(false);
|
|
2001
|
+
disarmHoverClose();
|
|
1976
2002
|
setHoverCard({
|
|
1977
2003
|
title: info.title,
|
|
1978
2004
|
sub: info.sub,
|
|
@@ -1981,21 +2007,177 @@ window.__ModuleLoader__.load({
|
|
|
1981
2007
|
ghost: info.ghost,
|
|
1982
2008
|
top: r.top,
|
|
1983
2009
|
left: r.left,
|
|
1984
|
-
right: r.right
|
|
2010
|
+
right: r.right,
|
|
2011
|
+
// the row this card describes: pinning keeps it, and the menu labels itself from it
|
|
2012
|
+
key: info.key,
|
|
2013
|
+
idx: info.idx
|
|
1985
2014
|
});
|
|
1986
2015
|
}, 260);
|
|
1987
2016
|
};
|
|
1988
|
-
// leaving fades the card out instead of dropping it on the next frame
|
|
1989
|
-
|
|
2017
|
+
// leaving fades the card out instead of dropping it on the next frame — but only
|
|
2018
|
+
// after a grace period, because the card carries the pin and the pointer has to be
|
|
2019
|
+
// able to travel from the row onto it (an immediate fade made the pin unreachable).
|
|
2020
|
+
var hoverEnd = function (immediate) {
|
|
1990
2021
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
2022
|
+
if (hoverGraceTimerRef.current) clearTimeout(hoverGraceTimerRef.current);
|
|
1991
2023
|
if (hoverCloseTimerRef.current) clearTimeout(hoverCloseTimerRef.current);
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2024
|
+
var grace = immediate ? 0 : HOVER_GRACE_MS;
|
|
2025
|
+
hoverGraceTimerRef.current = setTimeout(function () {
|
|
2026
|
+
hoverGraceTimerRef.current = null;
|
|
2027
|
+
setHoverClosing(true);
|
|
2028
|
+
hoverCloseTimerRef.current = setTimeout(function () {
|
|
2029
|
+
hoverCloseTimerRef.current = null;
|
|
2030
|
+
setHoverCard(null);
|
|
2031
|
+
setHoverClosing(false);
|
|
2032
|
+
}, 240);
|
|
2033
|
+
}, grace);
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
// ---- pinned preview + the row menu (0.8.0) --------------------------------
|
|
2037
|
+
// Pinning freezes one card where it is, so the pointer can move on and a second
|
|
2038
|
+
// row can be hovered next to it: two previews read side by side. One pin at a
|
|
2039
|
+
// time — pinning another row replaces it (the newest pin is the one that stays).
|
|
2040
|
+
var _sPin = react.useState(null);
|
|
2041
|
+
var pinnedCard = _sPin[0];
|
|
2042
|
+
var setPinnedCard = _sPin[1];
|
|
2043
|
+
var pinFromCard = function (card, opts) {
|
|
2044
|
+
if (!card) return;
|
|
2045
|
+
if (pinCloseTimerRef.current) { clearTimeout(pinCloseTimerRef.current); pinCloseTimerRef.current = null; }
|
|
2046
|
+
setPinnedClosing(false);
|
|
2047
|
+
setPinnedCard({
|
|
2048
|
+
title: card.title,
|
|
2049
|
+
sub: card.sub,
|
|
2050
|
+
preview: card.preview,
|
|
2051
|
+
meta: card.meta,
|
|
2052
|
+
ghost: card.ghost,
|
|
2053
|
+
key: card.key,
|
|
2054
|
+
top: card.top,
|
|
2055
|
+
left: card.left,
|
|
2056
|
+
right: card.right,
|
|
2057
|
+
// adopting the card that is already on screen must not re-play its entrance
|
|
2058
|
+
// animation, or pinning would blink the very card the reader is looking at
|
|
2059
|
+
fresh: !!(opts && opts.fresh)
|
|
2060
|
+
});
|
|
2061
|
+
// the card the reader is looking at BECOMES the pinned one: the hover card is
|
|
2062
|
+
// dropped in the same frame, so pinning never moves what is on screen
|
|
2063
|
+
if (hoverCloseTimerRef.current) { clearTimeout(hoverCloseTimerRef.current); hoverCloseTimerRef.current = null; }
|
|
2064
|
+
setHoverClosing(false);
|
|
2065
|
+
setHoverCard(null);
|
|
1998
2066
|
};
|
|
2067
|
+
// The pinned card STAYS where it was put: scrolling the outline leaves it alone.
|
|
2068
|
+
// Dropping it runs the same fade the hover card uses, so nothing ever pops out.
|
|
2069
|
+
var _sPinClose = react.useState(false);
|
|
2070
|
+
var pinnedClosing = _sPinClose[0];
|
|
2071
|
+
var setPinnedClosing = _sPinClose[1];
|
|
2072
|
+
var pinCloseTimerRef = react.useRef(null);
|
|
2073
|
+
var unpinCard = function () {
|
|
2074
|
+
if (!pinnedCard || pinCloseTimerRef.current) return;
|
|
2075
|
+
setPinnedClosing(true);
|
|
2076
|
+
pinCloseTimerRef.current = setTimeout(function () {
|
|
2077
|
+
pinCloseTimerRef.current = null;
|
|
2078
|
+
setPinnedCard(null);
|
|
2079
|
+
setPinnedClosing(false);
|
|
2080
|
+
}, 180);
|
|
2081
|
+
};
|
|
2082
|
+
// the pinned card measures itself so the second card can be placed against it
|
|
2083
|
+
var pinnedRef = react.useRef(null);
|
|
2084
|
+
// the card keeps the row it describes, so the menu can say pin/unpin for it
|
|
2085
|
+
var isPinned = function (key) { return !!pinnedCard && !!key && pinnedCard.key === key; };
|
|
2086
|
+
|
|
2087
|
+
// A right-click on an outline row opens a small menu instead of the browser's:
|
|
2088
|
+
// pin/unpin this row's preview and jump to the end of its section.
|
|
2089
|
+
var _sMenu = react.useState(null);
|
|
2090
|
+
var rowMenu = _sMenu[0];
|
|
2091
|
+
var setRowMenu = _sMenu[1];
|
|
2092
|
+
// the menu closes through a short fade like every other card
|
|
2093
|
+
var _sMenuClose = react.useState(false);
|
|
2094
|
+
var menuClosing = _sMenuClose[0];
|
|
2095
|
+
var setMenuClosing = _sMenuClose[1];
|
|
2096
|
+
var menuCloseTimerRef = react.useRef(null);
|
|
2097
|
+
var openRowMenu = function (e, info, key, idx) {
|
|
2098
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
2099
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
2100
|
+
var x = e && typeof e.clientX === "number" ? e.clientX : 0;
|
|
2101
|
+
var y = e && typeof e.clientY === "number" ? e.clientY : 0;
|
|
2102
|
+
if (menuCloseTimerRef.current) { clearTimeout(menuCloseTimerRef.current); menuCloseTimerRef.current = null; }
|
|
2103
|
+
setMenuClosing(false);
|
|
2104
|
+
// pinning from the menu puts the card where the row is, not under the pointer
|
|
2105
|
+
var rect = e && e.currentTarget && e.currentTarget.getBoundingClientRect ? e.currentTarget.getBoundingClientRect() : null;
|
|
2106
|
+
setRowMenu({
|
|
2107
|
+
x: x,
|
|
2108
|
+
y: y,
|
|
2109
|
+
info: info,
|
|
2110
|
+
key: key,
|
|
2111
|
+
idx: idx,
|
|
2112
|
+
rowTop: rect ? rect.top : undefined,
|
|
2113
|
+
rowLeft: rect ? rect.left : undefined,
|
|
2114
|
+
rowRight: rect ? rect.right : undefined
|
|
2115
|
+
});
|
|
2116
|
+
};
|
|
2117
|
+
var closeRowMenu = function () {
|
|
2118
|
+
if (!rowMenu || menuCloseTimerRef.current) return;
|
|
2119
|
+
setMenuClosing(true);
|
|
2120
|
+
menuCloseTimerRef.current = setTimeout(function () {
|
|
2121
|
+
menuCloseTimerRef.current = null;
|
|
2122
|
+
setRowMenu(null);
|
|
2123
|
+
setMenuClosing(false);
|
|
2124
|
+
}, 150);
|
|
2125
|
+
};
|
|
2126
|
+
// Esc / a click anywhere else closes the menu and drops the pin; a pinned card
|
|
2127
|
+
// also goes away when the panel closes or the surface switches (below).
|
|
2128
|
+
// The listeners are bound ONCE and read the live state through a ref: when they were
|
|
2129
|
+
// re-bound on every state change the registered closure ran one step behind (Esc
|
|
2130
|
+
// closed the menu a second time and never reached the pin).
|
|
2131
|
+
var latestPin = react.useRef({});
|
|
2132
|
+
latestPin.current = {
|
|
2133
|
+
pinned: !!pinnedCard,
|
|
2134
|
+
menu: !!rowMenu,
|
|
2135
|
+
unpin: unpinCard,
|
|
2136
|
+
closeMenu: closeRowMenu
|
|
2137
|
+
};
|
|
2138
|
+
react.useEffect(function () {
|
|
2139
|
+
var onDown = function (ev) {
|
|
2140
|
+
var L = latestPin.current;
|
|
2141
|
+
if (!L.pinned && !L.menu) return;
|
|
2142
|
+
// a right-click is how the row menu opens: it must not drop the pin the reader
|
|
2143
|
+
// is comparing against
|
|
2144
|
+
if (ev.button === 2) return;
|
|
2145
|
+
var t = ev.target;
|
|
2146
|
+
if (t && t.closest && (t.closest("[data-dqt-pin]") || t.closest("[data-dqt-menu]"))) return;
|
|
2147
|
+
L.closeMenu();
|
|
2148
|
+
if (L.pinned && t && t.closest && t.closest("[data-dqt-pinned]")) return;
|
|
2149
|
+
L.unpin();
|
|
2150
|
+
};
|
|
2151
|
+
var onKey = function (ev) {
|
|
2152
|
+
var L = latestPin.current;
|
|
2153
|
+
if (ev.key !== "Escape") return;
|
|
2154
|
+
// Esc takes the menu first, then the pin; only when neither is up does it
|
|
2155
|
+
// reach the panel's own handler and close the panel.
|
|
2156
|
+
if (L.menu) { L.closeMenu(); ev.stopPropagation(); ev.preventDefault(); return; }
|
|
2157
|
+
if (L.pinned) { L.unpin(); ev.stopPropagation(); ev.preventDefault(); }
|
|
2158
|
+
};
|
|
2159
|
+
document.addEventListener("mousedown", onDown, true);
|
|
2160
|
+
document.addEventListener("keydown", onKey, true);
|
|
2161
|
+
return function () {
|
|
2162
|
+
document.removeEventListener("mousedown", onDown, true);
|
|
2163
|
+
document.removeEventListener("keydown", onKey, true);
|
|
2164
|
+
};
|
|
2165
|
+
}, []);
|
|
2166
|
+
// a pinned card belongs to the surface it was pinned on: closing the panel or
|
|
2167
|
+
// switching between the panel and the curtain drops it (through the same fade).
|
|
2168
|
+
// These act on the TRANSITION only — a plain re-run must never touch a live pin.
|
|
2169
|
+
var wasOpenRef = react.useRef(open);
|
|
2170
|
+
react.useEffect(function () {
|
|
2171
|
+
if (wasOpenRef.current === open) return;
|
|
2172
|
+
wasOpenRef.current = open;
|
|
2173
|
+
if (!open) unpinCard();
|
|
2174
|
+
}, [open]);
|
|
2175
|
+
var wasSheetRef = react.useRef(sheetOpen);
|
|
2176
|
+
react.useEffect(function () {
|
|
2177
|
+
if (wasSheetRef.current === sheetOpen) return;
|
|
2178
|
+
wasSheetRef.current = sheetOpen;
|
|
2179
|
+
unpinCard();
|
|
2180
|
+
}, [sheetOpen]);
|
|
1999
2181
|
|
|
2000
2182
|
// ---- the outline belongs to the CHAT view only: when the center column
|
|
2001
2183
|
// switches to another view (trajectory / context / plugin views), fade the
|
|
@@ -2969,7 +3151,7 @@ window.__ModuleLoader__.load({
|
|
|
2969
3151
|
autoGestureRef.current = Date.now(); // a wheel tick is a reader gesture
|
|
2970
3152
|
var el = listRef.current;
|
|
2971
3153
|
if (!el) return;
|
|
2972
|
-
hoverEnd();
|
|
3154
|
+
hoverEnd(true);
|
|
2973
3155
|
if (e.deltaY >= 0) {
|
|
2974
3156
|
// still scrolling down at the very end of the outline: say so. Outline
|
|
2975
3157
|
// mode only — the search list has its own persistent hint line.
|
|
@@ -3006,7 +3188,7 @@ window.__ModuleLoader__.load({
|
|
|
3006
3188
|
var upward = el.scrollTop < lastScrollTopRef.current - 2;
|
|
3007
3189
|
if (upward) {
|
|
3008
3190
|
if (hint === "more") setHint("");
|
|
3009
|
-
if (hoverCard) hoverEnd();
|
|
3191
|
+
if (hoverCard) hoverEnd(true);
|
|
3010
3192
|
}
|
|
3011
3193
|
lastScrollTopRef.current = el.scrollTop;
|
|
3012
3194
|
syncBackBtn(el); // reached the remembered place by hand? the button is done
|
|
@@ -4747,6 +4929,8 @@ window.__ModuleLoader__.load({
|
|
|
4747
4929
|
onOpenTurn: openTurn,
|
|
4748
4930
|
hoverStart: hoverStart,
|
|
4749
4931
|
hoverEnd: hoverEnd,
|
|
4932
|
+
// right-click on a row: pin its preview / jump to the end of its section
|
|
4933
|
+
rowMenu: openRowMenu,
|
|
4750
4934
|
// the curtain is wide: rows switch to a one-line, multi-part layout that
|
|
4751
4935
|
// puts the section's opening text out to the right instead of under the
|
|
4752
4936
|
// title (see renderItem / renderResultRow)
|
|
@@ -6009,7 +6193,10 @@ window.__ModuleLoader__.load({
|
|
|
6009
6193
|
// vanishing ON the divider line. `open` also drives it (fade + slide home) and
|
|
6010
6194
|
// `edgeGone` keeps the element in the tree until that motion has played.
|
|
6011
6195
|
var EDGE_W = 26;
|
|
6012
|
-
|
|
6196
|
+
// a narrow conversation column leaves no room for a handle floating over the text:
|
|
6197
|
+
// below the threshold it slides back into the divider (its usual motion), while the
|
|
6198
|
+
// droplet above the conversation stays put as the way in
|
|
6199
|
+
var edgeShown = !open && handleShown && chatViewActive && !narrowWindow;
|
|
6013
6200
|
var edgeLineLeft = viewport ? viewport.left : 0;
|
|
6014
6201
|
var edgeLineRight = viewport ? viewport.right + 52 : 60; // clear of the milestone rail
|
|
6015
6202
|
var edgeEl = (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
|
|
@@ -6197,42 +6384,110 @@ window.__ModuleLoader__.load({
|
|
|
6197
6384
|
// Rendered INSIDE the body portal (position: fixed) so the outline's own
|
|
6198
6385
|
// scroll container can never clip it, and pointer-events: none so it can
|
|
6199
6386
|
// never steal the hover that opened it.
|
|
6200
|
-
|
|
6201
|
-
|
|
6387
|
+
// One card body, used for both the hover card and the pinned one: same width,
|
|
6388
|
+
// same fields, same layering. `opts.pinned` turns it into the frozen copy (it
|
|
6389
|
+
// takes pointer events, so its text can be selected and its pin can be clicked);
|
|
6390
|
+
// the hover card stays click-through except for its pin button.
|
|
6391
|
+
var renderPreviewCard = function (card, opts) {
|
|
6202
6392
|
var cardW = 264;
|
|
6203
|
-
var
|
|
6204
|
-
var
|
|
6205
|
-
? Math.max(8,
|
|
6206
|
-
: Math.min(window.innerWidth - cardW - 8,
|
|
6207
|
-
|
|
6208
|
-
|
|
6393
|
+
var top = Math.max(8, Math.min(card.top - 4, window.innerHeight - 210));
|
|
6394
|
+
var left = dockRight
|
|
6395
|
+
? Math.max(8, card.left - cardW - 10)
|
|
6396
|
+
: Math.min(window.innerWidth - cardW - 8, card.right + 10);
|
|
6397
|
+
// With a card already pinned, a second one on the same band would cover it: put
|
|
6398
|
+
// the hover card on whichever side of the pinned card has more room (measured,
|
|
6399
|
+
// so a short pinned card does not push the second one further than needed).
|
|
6400
|
+
if (!opts.pinned && pinnedCard && pinnedVisible && !pinnedClosing) {
|
|
6401
|
+
var pinRect = pinnedRef.current && pinnedRef.current.getBoundingClientRect ? pinnedRef.current.getBoundingClientRect() : null;
|
|
6402
|
+
var pinTop = pinRect && pinRect.height > 0 ? pinRect.top : Math.max(8, Math.min(pinnedCard.top - 4, window.innerHeight - 210));
|
|
6403
|
+
var pinH = pinRect && pinRect.height > 0 ? pinRect.height : 210;
|
|
6404
|
+
var pinLeft = pinRect && pinRect.width > 0
|
|
6405
|
+
? pinRect.left
|
|
6406
|
+
: (dockRight ? Math.max(8, pinnedCard.left - cardW - 10) : Math.min(window.innerWidth - cardW - 8, pinnedCard.right + 10));
|
|
6407
|
+
var overlaps = Math.abs(pinLeft - left) < cardW && top < pinTop + pinH + 6 && pinTop < top + 210 + 6;
|
|
6408
|
+
if (overlaps) {
|
|
6409
|
+
var belowRoom = window.innerHeight - 8 - (pinTop + pinH + 8);
|
|
6410
|
+
var aboveRoom = pinTop - 8 - 8;
|
|
6411
|
+
top = belowRoom >= aboveRoom
|
|
6412
|
+
? Math.max(8, Math.min(pinTop + pinH + 8, window.innerHeight - 210 - 8))
|
|
6413
|
+
: Math.max(8, pinTop - 210 - 8);
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
var body = card.preview || card.sub || "";
|
|
6417
|
+
var kids = [
|
|
6209
6418
|
react_jsx_runtime.jsx("div", {
|
|
6210
|
-
style: {
|
|
6211
|
-
children:
|
|
6212
|
-
|
|
6419
|
+
style: { display: "flex", alignItems: "flex-start", gap: 6 },
|
|
6420
|
+
children: [
|
|
6421
|
+
react_jsx_runtime.jsx("div", {
|
|
6422
|
+
style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
6423
|
+
children: card.title
|
|
6424
|
+
}, "hc-t"),
|
|
6425
|
+
react_jsx_runtime.jsx("button", {
|
|
6426
|
+
"data-dqt-pin": opts.pinned ? "on" : "off",
|
|
6427
|
+
title: opts.pinned ? T("preview.unpin") : T("preview.pin"),
|
|
6428
|
+
onClick: function (e) {
|
|
6429
|
+
e.preventDefault();
|
|
6430
|
+
e.stopPropagation();
|
|
6431
|
+
if (opts.pinned) unpinCard();
|
|
6432
|
+
else pinFromCard(card);
|
|
6433
|
+
},
|
|
6434
|
+
style: {
|
|
6435
|
+
flex: "none",
|
|
6436
|
+
width: 20,
|
|
6437
|
+
height: 20,
|
|
6438
|
+
marginTop: -2,
|
|
6439
|
+
marginRight: -4,
|
|
6440
|
+
display: "flex",
|
|
6441
|
+
alignItems: "center",
|
|
6442
|
+
justifyContent: "center",
|
|
6443
|
+
border: "none",
|
|
6444
|
+
borderRadius: 6,
|
|
6445
|
+
background: opts.pinned ? C.hover : "transparent",
|
|
6446
|
+
color: opts.pinned ? C.text : C.muted,
|
|
6447
|
+
cursor: "pointer",
|
|
6448
|
+
// the hover card is click-through except for this button, so reaching
|
|
6449
|
+
// for it does not depend on the row staying hovered
|
|
6450
|
+
pointerEvents: "auto",
|
|
6451
|
+
padding: 0,
|
|
6452
|
+
fontSize: 11,
|
|
6453
|
+
lineHeight: "20px"
|
|
6454
|
+
},
|
|
6455
|
+
children: opts.pinned ? "\u25c9" : "\u25cb"
|
|
6456
|
+
}, "hc-pin")
|
|
6457
|
+
]
|
|
6458
|
+
}, "hc-head")
|
|
6213
6459
|
];
|
|
6214
|
-
if (
|
|
6215
|
-
|
|
6460
|
+
if (card.meta) {
|
|
6461
|
+
kids.push(react_jsx_runtime.jsx("div", {
|
|
6216
6462
|
style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
6217
|
-
children:
|
|
6463
|
+
children: card.meta
|
|
6218
6464
|
}, "hc-m"));
|
|
6219
6465
|
}
|
|
6220
|
-
|
|
6466
|
+
kids.push(react_jsx_runtime.jsx("div", {
|
|
6221
6467
|
style: { fontSize: 11, lineHeight: "16px", color: C.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
|
|
6222
|
-
children:
|
|
6468
|
+
children: body || (card.ghost ? T("turn.loadTip") : "")
|
|
6223
6469
|
}, "hc-b"));
|
|
6224
|
-
if (
|
|
6225
|
-
|
|
6470
|
+
if (card.ghost) {
|
|
6471
|
+
kids.push(react_jsx_runtime.jsx("div", {
|
|
6226
6472
|
style: { fontSize: 10, color: C.muted, opacity: 0.7, marginTop: 4 },
|
|
6227
6473
|
children: T("preview.truncated")
|
|
6228
6474
|
}, "hc-n"));
|
|
6229
6475
|
}
|
|
6230
|
-
|
|
6231
|
-
className: "dqt-hover" + (
|
|
6476
|
+
return react_jsx_runtime.jsx("div", {
|
|
6477
|
+
className: "dqt-hover" + (opts.closing ? " dqt-hover-closing" : "") + (opts.pinned ? " dqt-hover-pinned" : "")
|
|
6478
|
+
// a card adopted from the hover card is already on screen: skip its entrance
|
|
6479
|
+
+ (opts.pinned && !card.fresh ? " dqt-hover-static" : ""),
|
|
6480
|
+
"data-dqt-pinned": opts.pinned ? "on" : undefined,
|
|
6481
|
+
ref: opts.pinned ? pinnedRef : undefined,
|
|
6482
|
+
onMouseEnter: opts.pinned ? undefined : function () {
|
|
6483
|
+
// the pointer reached the card: keep it open so its pin can be clicked
|
|
6484
|
+
disarmHoverClose();
|
|
6485
|
+
},
|
|
6486
|
+
onMouseLeave: opts.pinned ? undefined : function () { hoverEnd(); },
|
|
6232
6487
|
style: {
|
|
6233
6488
|
position: "fixed",
|
|
6234
|
-
top:
|
|
6235
|
-
left:
|
|
6489
|
+
top: top,
|
|
6490
|
+
left: left,
|
|
6236
6491
|
width: cardW,
|
|
6237
6492
|
padding: "8px 10px",
|
|
6238
6493
|
borderRadius: 8,
|
|
@@ -6243,11 +6498,107 @@ window.__ModuleLoader__.load({
|
|
|
6243
6498
|
boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
|
|
6244
6499
|
// above the panel and the curtain — it is a sibling of both in this
|
|
6245
6500
|
// portal, and both sit below it in the layer band we claim
|
|
6246
|
-
zIndex: Z_HOVER,
|
|
6247
|
-
|
|
6501
|
+
zIndex: Z_HOVER + (opts.pinned ? 1 : 0),
|
|
6502
|
+
// a fading card is inert: it must not swallow the click that dismissed it.
|
|
6503
|
+
// While it is up the card IS clickable — it carries the pin, and the pointer
|
|
6504
|
+
// has to be able to get there from the row.
|
|
6505
|
+
pointerEvents: opts.closing ? "none" : "auto",
|
|
6506
|
+
userSelect: opts.pinned ? "text" : "none"
|
|
6507
|
+
},
|
|
6508
|
+
children: kids
|
|
6509
|
+
}, opts.pinned ? "pinned-card" : "hover-card");
|
|
6510
|
+
};
|
|
6511
|
+
|
|
6512
|
+
// The pinned card is FROZEN where it was put: scrolling the outline leaves it alone,
|
|
6513
|
+
// and it only leaves the screen through the same fade the hover card uses.
|
|
6514
|
+
var pinnedVisible = !!(pinnedCard && open);
|
|
6515
|
+
var pinnedEl = pinnedVisible
|
|
6516
|
+
? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing })
|
|
6517
|
+
: null;
|
|
6518
|
+
var hoverEl = null;
|
|
6519
|
+
if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing });
|
|
6520
|
+
|
|
6521
|
+
// ---- the row menu ---------------------------------------------------------
|
|
6522
|
+
// Small, fixed-position, keyboard-reachable: ↑/↓ move, Enter runs, Esc closes.
|
|
6523
|
+
var rowMenuEl = null;
|
|
6524
|
+
if (rowMenu && open) {
|
|
6525
|
+
var menuW = 208;
|
|
6526
|
+
var menuH = 76;
|
|
6527
|
+
var menuLeft = Math.max(8, Math.min(rowMenu.x, window.innerWidth - menuW - 8));
|
|
6528
|
+
var menuTop = Math.max(8, Math.min(rowMenu.y, window.innerHeight - menuH - 8));
|
|
6529
|
+
var menuItems = [
|
|
6530
|
+
{
|
|
6531
|
+
id: "pin",
|
|
6532
|
+
label: isPinned(rowMenu.key) ? T("menu.unpin") : T("menu.pin"),
|
|
6533
|
+
run: function () {
|
|
6534
|
+
if (isPinned(rowMenu.key)) unpinCard();
|
|
6535
|
+
else pinFromCard({
|
|
6536
|
+
title: rowMenu.info.title,
|
|
6537
|
+
sub: rowMenu.info.sub,
|
|
6538
|
+
preview: rowMenu.info.preview,
|
|
6539
|
+
meta: rowMenu.info.meta,
|
|
6540
|
+
ghost: rowMenu.info.ghost,
|
|
6541
|
+
key: rowMenu.key,
|
|
6542
|
+
// pin it where its ROW is, not where the right-click landed
|
|
6543
|
+
top: rowMenu.rowTop !== undefined ? rowMenu.rowTop : rowMenu.y,
|
|
6544
|
+
left: rowMenu.rowLeft !== undefined ? rowMenu.rowLeft : rowMenu.x,
|
|
6545
|
+
right: rowMenu.rowRight !== undefined ? rowMenu.rowRight : rowMenu.x
|
|
6546
|
+
}, { fresh: true });
|
|
6547
|
+
closeRowMenu();
|
|
6548
|
+
}
|
|
6549
|
+
},
|
|
6550
|
+
{
|
|
6551
|
+
id: "end",
|
|
6552
|
+
label: T("menu.end"),
|
|
6553
|
+
run: function () {
|
|
6554
|
+
var key = rowMenu.key;
|
|
6555
|
+
var idx = rowMenu.idx;
|
|
6556
|
+
closeRowMenu();
|
|
6557
|
+
if (typeof endJumpRow === "function") endJumpRow(key, idx === undefined ? 0 : idx);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
];
|
|
6561
|
+
rowMenuEl = react_jsx_runtime.jsx("div", {
|
|
6562
|
+
"data-dqt-menu": "on",
|
|
6563
|
+
className: "dqt-rowmenu" + (menuClosing ? " dqt-rowmenu-closing" : ""),
|
|
6564
|
+
role: "menu",
|
|
6565
|
+
style: {
|
|
6566
|
+
position: "fixed",
|
|
6567
|
+
top: menuTop,
|
|
6568
|
+
left: menuLeft,
|
|
6569
|
+
width: menuW,
|
|
6570
|
+
padding: 4,
|
|
6571
|
+
borderRadius: 8,
|
|
6572
|
+
background: C.panelBg,
|
|
6573
|
+
color: C.text,
|
|
6574
|
+
border: "1px solid " + C.panelBorder,
|
|
6575
|
+
boxShadow: "0 10px 28px rgba(0,0,0,0.32)",
|
|
6576
|
+
zIndex: Z_HOVER + 2,
|
|
6577
|
+
// a fading menu must not swallow the click that dismissed it
|
|
6578
|
+
pointerEvents: menuClosing ? "none" : "auto"
|
|
6248
6579
|
},
|
|
6249
|
-
children:
|
|
6250
|
-
|
|
6580
|
+
children: menuItems.map(function (item) {
|
|
6581
|
+
return react_jsx_runtime.jsx("button", {
|
|
6582
|
+
role: "menuitem",
|
|
6583
|
+
"data-dqt-menu-item": item.id,
|
|
6584
|
+
onClick: function (e) { e.preventDefault(); e.stopPropagation(); item.run(); },
|
|
6585
|
+
style: {
|
|
6586
|
+
display: "block",
|
|
6587
|
+
width: "100%",
|
|
6588
|
+
textAlign: "left",
|
|
6589
|
+
padding: "6px 8px",
|
|
6590
|
+
border: "none",
|
|
6591
|
+
borderRadius: 6,
|
|
6592
|
+
background: "transparent",
|
|
6593
|
+
color: C.text,
|
|
6594
|
+
cursor: "pointer",
|
|
6595
|
+
fontSize: 12,
|
|
6596
|
+
lineHeight: "18px"
|
|
6597
|
+
},
|
|
6598
|
+
children: item.label
|
|
6599
|
+
}, "menu-" + item.id);
|
|
6600
|
+
})
|
|
6601
|
+
}, "row-menu");
|
|
6251
6602
|
}
|
|
6252
6603
|
|
|
6253
6604
|
// ---- the curtain shell ---------------------------------------------------
|
|
@@ -6333,7 +6684,7 @@ window.__ModuleLoader__.load({
|
|
|
6333
6684
|
|
|
6334
6685
|
return react_dom.createPortal(
|
|
6335
6686
|
react_jsx_runtime.jsx(ErrorBoundary, {
|
|
6336
|
-
children: [sheetShellEl, edgeEl, topHandleEl, hoverEl]
|
|
6687
|
+
children: [sheetShellEl, edgeEl, topHandleEl, pinnedEl, hoverEl, rowMenuEl]
|
|
6337
6688
|
}),
|
|
6338
6689
|
document.body
|
|
6339
6690
|
);
|
|
@@ -6419,7 +6770,10 @@ window.__ModuleLoader__.load({
|
|
|
6419
6770
|
sub: n.sub || "",
|
|
6420
6771
|
preview: n.preview || "",
|
|
6421
6772
|
meta: [n.time, path ? path.path : ""].filter(Boolean).join(" · "),
|
|
6422
|
-
ghost: false
|
|
6773
|
+
ghost: false,
|
|
6774
|
+
// what pinning and the row menu need to act on this very row
|
|
6775
|
+
key: n.key,
|
|
6776
|
+
idx: n.idx
|
|
6423
6777
|
};
|
|
6424
6778
|
var rows;
|
|
6425
6779
|
if (wide) {
|
|
@@ -6520,6 +6874,9 @@ window.__ModuleLoader__.load({
|
|
|
6520
6874
|
if (b) b.style.opacity = "0";
|
|
6521
6875
|
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
6522
6876
|
},
|
|
6877
|
+
onContextMenu: function (e) {
|
|
6878
|
+
if (extra && extra.rowMenu) extra.rowMenu(e, hoverInfo, n.key, n.idx);
|
|
6879
|
+
},
|
|
6523
6880
|
"data-jump-key": n.key,
|
|
6524
6881
|
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
6525
6882
|
// keyboard navigation walks the ORDER of these rows inside the list
|
|
@@ -7646,9 +8003,17 @@ window.__ModuleLoader__.load({
|
|
|
7646
8003
|
// re-renders (which rewrite inline styles) cannot restart or cancel them
|
|
7647
8004
|
".dqt-levels-pop{animation:dqt-pop-in 0.22s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
7648
8005
|
// hover preview card: fades in place, and fades OUT when the pointer
|
|
7649
|
-
// leaves (both as stylesheet classes so re-renders cannot cancel them)
|
|
8006
|
+
// leaves (both as stylesheet classes so re-renders cannot cancel them).
|
|
8007
|
+
// The pinned card wears the same two classes, so pinning, unpinning, Esc, a
|
|
8008
|
+
// click outside and a panel close all animate; `.dqt-hover-static` is for the
|
|
8009
|
+
// card that is ADOPTED from the hover card (it is already on screen, so it must
|
|
8010
|
+
// not re-play the entrance and blink).
|
|
7650
8011
|
".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
|
|
7651
|
-
".dqt-hover-
|
|
8012
|
+
".dqt-hover-static{animation:none}" +
|
|
8013
|
+
".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none}" +
|
|
8014
|
+
// the outline row menu: same vocabulary as the level picker (a small pop)
|
|
8015
|
+
".dqt-rowmenu{animation:dqt-pop-in 0.14s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
8016
|
+
".dqt-rowmenu-closing{animation:dqt-pop-out 0.14s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none;will-change:transform,opacity}" +
|
|
7652
8017
|
// transient edge banner over the bottom of the outline list
|
|
7653
8018
|
"@keyframes dqt-toast-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}" +
|
|
7654
8019
|
"@keyframes dqt-toast-out{from{opacity:1;transform:none}to{opacity:0;transform:none}}" +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, a level filter, a full-width curtain, jump-to-end buttons, title/full-text/cross-session search and a Chinese/English interface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|