dsh-quick-toc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +50 -0
- package/README.md +50 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +948 -0
- package/lib/index.js +8 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LyaxZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# dsh-quick-toc
|
|
2
|
+
|
|
3
|
+
> **English** | [中文](README.md)
|
|
4
|
+
|
|
5
|
+
A quick conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): extracts Markdown headings (H1–H6) from AI replies into a navigable outline panel, grouped by conversation turn, with auto-follow highlighting.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Turn-grouped outline** — each user message + its AI replies form one group, with the group's end time as the header
|
|
10
|
+
- **Auto-follow highlight** — as you scroll the conversation, the turns visible in the viewport light up in the outline (multiple at once); the outline auto-loads and scrolls to keep them visible
|
|
11
|
+
- **Smooth jump** — click a heading to glide to the exact heading position in the conversation
|
|
12
|
+
- **Dockable & resizable** — dock left or right (drag the top bar), resize from the edges/corner, collapse to a draggable edge handle; panel size/position remembered
|
|
13
|
+
- **Scrollbar follows the dock** — scrollbar sits on the left when docked left, right when docked right
|
|
14
|
+
- **Paged rendering** — shows the latest groups first; scrolling to the top of the outline loads older ones
|
|
15
|
+
- **Markdown-aware titles** — inline `**bold**`, *italic*, `` `code` ``, `[links](url)`, `~~strike~~` are stripped from heading text
|
|
16
|
+
- Auto-hides when the conversation has no headings; works in light/dark themes
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
With the DSH CLI:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dsh plugin --profile web add github:LyaxZ/dsh-quick-toc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or, for a local checkout:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
dsh plugin --profile web add <path-to-this-folder>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Restart DSH (double-click `restart-dsh.bat` on Windows) and open the Web UI. The outline is collapsed by default — click the small edge handle on the left side of the conversation to expand it.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
- Click a heading in the outline to jump to that heading in the conversation
|
|
37
|
+
- Drag the top bar to move the panel; use the **◀ / ▶** button to dock left/right
|
|
38
|
+
- Drag the right edge (width), bottom edge (height) or the bottom-right corner (both) to resize
|
|
39
|
+
- Scroll the outline to the top to load older groups
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
- `lib/client.js` — all UI logic (browser side)
|
|
44
|
+
- `lib/index.js` — host-side no-op entry
|
|
45
|
+
- `cordis.patch.yml` — loader patch (official DSH bundle format)
|
|
46
|
+
- Client changes hot-load after a browser hard refresh; loader/patch changes need a DSH restart
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT © 2026 LyaxZ
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# dsh-quick-toc
|
|
2
|
+
|
|
3
|
+
> [English](README.en.md) | **中文**
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness(DSH)对话大纲插件**:把 AI 回复中的 Markdown 标题(H1–H6)提取成可导航的大纲面板,按对话回合分组,自动跟随阅读位置。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示该组最后一条消息的结束时间
|
|
10
|
+
- **自动跟随高亮** —— 滚动对话框时,视口内可见的回合在大纲中自动点亮(多组可同时点亮);大纲自动加载并滚动,保证正在读的组始终可见
|
|
11
|
+
- **平滑跳转** —— 点击标题平滑滚动到对话中该标题的准确位置
|
|
12
|
+
- **可停靠、可缩放** —— 左右停靠(拖顶部横条移动)、边缘/角部拖拽调整大小、收起成可拖动的边缘把手;大小与位置自动记忆
|
|
13
|
+
- **滚动条跟随停靠方向** —— 停靠左侧时滚动条在左,停靠右侧时在右
|
|
14
|
+
- **分页渲染** —— 默认显示最近几组,大纲滚动到顶部自动加载更早的组
|
|
15
|
+
- **Markdown 清理** —— 标题中的 `**加粗**`、*斜体*、`` `代码` ``、`[链接](url)`、`~~删除线~~` 等行内标记自动剥离
|
|
16
|
+
- 对话无标题时自动隐藏;适配深色 / 浅色主题
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
通过 DSH CLI 安装(仓库路径替换为实际值):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dsh plugin --profile web add github:LyaxZ/dsh-quick-toc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
或本地目录安装:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
dsh plugin --profile web add <本目录路径>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
重启 DSH(Windows 上双击 `restart-dsh.bat`)后打开 Web UI。大纲默认收起——点击对话区左侧边缘的小把手展开。
|
|
33
|
+
|
|
34
|
+
## 使用
|
|
35
|
+
|
|
36
|
+
- 点击大纲标题跳转到对话中对应位置
|
|
37
|
+
- 拖动顶部横条移动面板;点 **◀ / ▶** 按钮切换左右停靠
|
|
38
|
+
- 拖右边缘调宽度、下边缘调高度、右下角同时调
|
|
39
|
+
- 大纲滚动到顶部可加载更早的组
|
|
40
|
+
|
|
41
|
+
## 开发
|
|
42
|
+
|
|
43
|
+
- `lib/client.js` —— 全部 UI 逻辑(浏览器端)
|
|
44
|
+
- `lib/index.js` —— 宿主端空入口
|
|
45
|
+
- `cordis.patch.yml` —— loader patch(符合官方 bundle 规范)
|
|
46
|
+
- 改 client.js 后浏览器硬刷新即可生效;改 loader / patch 需重启 DSH
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT © 2026 LyaxZ
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-quick-toc",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
require("@deepseek-ai/dsh-client-runtime/client");
|
|
10
|
+
|
|
11
|
+
// ---------- constants ----------
|
|
12
|
+
var PANEL_WIDTH = 288;
|
|
13
|
+
var Z_BASE = 950;
|
|
14
|
+
var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
|
|
15
|
+
|
|
16
|
+
// ---------- markdown helpers ----------
|
|
17
|
+
function extractReplyText(node) {
|
|
18
|
+
var blocks = node && node.data && node.data.blocks;
|
|
19
|
+
if (!Array.isArray(blocks)) return "";
|
|
20
|
+
var out = "";
|
|
21
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
22
|
+
var b = blocks[i];
|
|
23
|
+
if (b && typeof b === "object" && (b.kind === "text" || b.type === "text") && typeof b.text === "string") {
|
|
24
|
+
out += b.text + "\n";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// strip inline markdown from a heading title: **bold**, *italic*,
|
|
31
|
+
// `code`, ~~strike~~, [link](url) -> link text
|
|
32
|
+
function cleanTitle(t) {
|
|
33
|
+
return t
|
|
34
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
35
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
36
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
37
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
38
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
39
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
40
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
41
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseHeadings(text) {
|
|
46
|
+
var items = [];
|
|
47
|
+
var re = /^(#{1,6})\s+(.+?)\s*#*\s*$/gm;
|
|
48
|
+
var m;
|
|
49
|
+
while ((m = re.exec(text)) !== null) {
|
|
50
|
+
items.push({ level: m[1].length, title: cleanTitle(m[2].trim()) });
|
|
51
|
+
}
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildTree(headings) {
|
|
56
|
+
var root = { level: 0, children: [] };
|
|
57
|
+
var stack = [root];
|
|
58
|
+
for (var i = 0; i < headings.length; i++) {
|
|
59
|
+
var h = headings[i];
|
|
60
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) stack.pop();
|
|
61
|
+
var node = { level: h.level, title: h.title, key: h.key, idx: h.idx, children: [] };
|
|
62
|
+
stack[stack.length - 1].children.push(node);
|
|
63
|
+
stack.push(node);
|
|
64
|
+
}
|
|
65
|
+
return root.children;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findRow(key) {
|
|
69
|
+
if (!key) return null;
|
|
70
|
+
var rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
71
|
+
// 1) exact dataset match
|
|
72
|
+
for (var i = 0; i < rows.length; i++) {
|
|
73
|
+
if (rows[i].dataset && rows[i].dataset.chatAnchorKey === key) return rows[i];
|
|
74
|
+
}
|
|
75
|
+
// 2) CSS attribute selector (keys may contain special chars)
|
|
76
|
+
try {
|
|
77
|
+
var el = document.querySelector('[data-chat-anchor-key="' + window.CSS.escape(key) + '"]');
|
|
78
|
+
if (el) return el;
|
|
79
|
+
} catch (e) {}
|
|
80
|
+
// 3) contains match (one side may be prefixed/suffixed)
|
|
81
|
+
for (var j = 0; j < rows.length; j++) {
|
|
82
|
+
var k = rows[j].dataset && rows[j].dataset.chatAnchorKey;
|
|
83
|
+
if (k && (k.indexOf(key) >= 0 || key.indexOf(k) >= 0)) return rows[j];
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------- theme colors: DSH CSS variables (adapt to light/dark) ----------
|
|
89
|
+
var C = {
|
|
90
|
+
panelBg: "var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96))",
|
|
91
|
+
panelBorder: "var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25))",
|
|
92
|
+
text: "var(--dsw-alias-label-primary, #e8eaee)",
|
|
93
|
+
muted: "var(--dsw-alias-label-secondary, #9aa0ab)",
|
|
94
|
+
accent: "var(--dsw-alias-brand-primary, #4f8cff)",
|
|
95
|
+
hover: "var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))",
|
|
96
|
+
chip: "rgba(79, 140, 255, 0.18)"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---------- error boundary ----------
|
|
100
|
+
var ErrorBoundary = (function () {
|
|
101
|
+
if (!react.Component) return function (p) { return p.children; };
|
|
102
|
+
function EB(props) {
|
|
103
|
+
this.props = props;
|
|
104
|
+
this.state = { err: null };
|
|
105
|
+
}
|
|
106
|
+
EB.prototype = Object.create(react.Component.prototype);
|
|
107
|
+
EB.prototype.constructor = EB;
|
|
108
|
+
EB.getDerivedStateFromError = function (e) { return { err: e }; };
|
|
109
|
+
EB.prototype.componentDidCatch = function (e) { console.error("[dsh-quick-toc] render error:", e); };
|
|
110
|
+
EB.prototype.render = function () {
|
|
111
|
+
if (this.state.err) {
|
|
112
|
+
var msg = this.state.err && this.state.err.message ? this.state.err.message : String(this.state.err);
|
|
113
|
+
return react_jsx_runtime.jsx("div", {
|
|
114
|
+
style: {
|
|
115
|
+
position: "fixed",
|
|
116
|
+
top: 80,
|
|
117
|
+
right: 16,
|
|
118
|
+
zIndex: Z_BASE + 1,
|
|
119
|
+
background: "rgba(120,30,30,0.95)",
|
|
120
|
+
border: "1px solid rgba(255,120,120,0.4)",
|
|
121
|
+
color: "#ffdada",
|
|
122
|
+
borderRadius: 8,
|
|
123
|
+
padding: "10px 14px",
|
|
124
|
+
fontSize: 12,
|
|
125
|
+
maxWidth: 340,
|
|
126
|
+
wordBreak: "break-all"
|
|
127
|
+
},
|
|
128
|
+
children: "dsh-quick-toc 面板出错: " + msg
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return this.props.children;
|
|
132
|
+
};
|
|
133
|
+
return EB;
|
|
134
|
+
})();
|
|
135
|
+
|
|
136
|
+
// ---------- component ----------
|
|
137
|
+
function OutlinePanel(props) {
|
|
138
|
+
var useSession = props.useSession;
|
|
139
|
+
if (!useSession) {
|
|
140
|
+
console.warn("[dsh-quick-toc] useSession prop missing");
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
var order = useSession(function (s) { return s.chat.order; });
|
|
144
|
+
var nodes = useSession(function (s) { return s.chat.nodes; });
|
|
145
|
+
|
|
146
|
+
// ---- hooks (ALL before any conditional return) ----
|
|
147
|
+
// collapsed by default; user expands via the edge handle (default dock: left)
|
|
148
|
+
var _s1 = react.useState(false);
|
|
149
|
+
var open = _s1[0];
|
|
150
|
+
var setOpen = _s1[1];
|
|
151
|
+
|
|
152
|
+
// conversation area measurement
|
|
153
|
+
var _s2 = react.useState(null);
|
|
154
|
+
var viewport = _s2[0];
|
|
155
|
+
var setViewport = _s2[1];
|
|
156
|
+
react.useEffect(function () {
|
|
157
|
+
var compute = function () {
|
|
158
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
159
|
+
if (sp) {
|
|
160
|
+
var r = sp.getBoundingClientRect();
|
|
161
|
+
setViewport({ left: r.left, right: window.innerWidth - r.right, top: r.top, height: r.height });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
compute();
|
|
165
|
+
window.addEventListener("resize", compute);
|
|
166
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
167
|
+
var obs = null;
|
|
168
|
+
if (typeof ResizeObserver !== "undefined" && sp) {
|
|
169
|
+
obs = new ResizeObserver(compute);
|
|
170
|
+
obs.observe(sp);
|
|
171
|
+
}
|
|
172
|
+
return function () {
|
|
173
|
+
window.removeEventListener("resize", compute);
|
|
174
|
+
if (obs) obs.disconnect();
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
// dock side: 'left' | 'right' (persisted; migrates old dsh-contents /
|
|
179
|
+
// dsh-dagang keys, default left)
|
|
180
|
+
var DOCK_KEY = "dsh-quick-toc.dock.v2";
|
|
181
|
+
var OLD_DOCK_KEYS = ["dsh-contents.dock.v2", "dsh-dagang.dock.v2"];
|
|
182
|
+
var _s3 = react.useState(function () {
|
|
183
|
+
try {
|
|
184
|
+
var d = localStorage.getItem(DOCK_KEY);
|
|
185
|
+
if (d === "left" || d === "right") return d;
|
|
186
|
+
for (var oi = 0; oi < OLD_DOCK_KEYS.length; oi++) {
|
|
187
|
+
var old = localStorage.getItem(OLD_DOCK_KEYS[oi]);
|
|
188
|
+
if (old === "left" || old === "right") { localStorage.setItem(DOCK_KEY, old); return old; }
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {}
|
|
191
|
+
return "left";
|
|
192
|
+
});
|
|
193
|
+
var dock = _s3[0];
|
|
194
|
+
var setDock = _s3[1];
|
|
195
|
+
var toggleDock = function () {
|
|
196
|
+
var next = dock === "right" ? "left" : "right";
|
|
197
|
+
setDock(next);
|
|
198
|
+
try { localStorage.setItem(DOCK_KEY, next); } catch (e) {}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// vertical drag offset (persisted; migrates old dsh-contents / dsh-dagang keys)
|
|
202
|
+
var PANEL_Y_KEY = "dsh-quick-toc.panelY.v1";
|
|
203
|
+
var OLD_PANEL_Y_KEYS = ["dsh-contents.panelY.v1", "dsh-dagang.panelY.v1"];
|
|
204
|
+
var _s4 = react.useState(function () {
|
|
205
|
+
try {
|
|
206
|
+
var v = localStorage.getItem(PANEL_Y_KEY);
|
|
207
|
+
if (v !== null && isFinite(Number(v))) return Number(v);
|
|
208
|
+
for (var oi = 0; oi < OLD_PANEL_Y_KEYS.length; oi++) {
|
|
209
|
+
var old = localStorage.getItem(OLD_PANEL_Y_KEYS[oi]);
|
|
210
|
+
if (old !== null && isFinite(Number(old))) { localStorage.setItem(PANEL_Y_KEY, old); return Number(old); }
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
return 0;
|
|
214
|
+
});
|
|
215
|
+
var panelY = _s4[0];
|
|
216
|
+
var setPanelY = _s4[1];
|
|
217
|
+
var handleRef = react.useRef(null); // panel top drag bar
|
|
218
|
+
var edgeRef = react.useRef(null); // edge collapse handle
|
|
219
|
+
|
|
220
|
+
// handle reveal: after the panel fully slides away, the handle fades+pops in
|
|
221
|
+
var _s5 = react.useState(false);
|
|
222
|
+
var handleShown = _s5[0];
|
|
223
|
+
var setHandleShown = _s5[1];
|
|
224
|
+
react.useEffect(function () {
|
|
225
|
+
if (!open) {
|
|
226
|
+
setHandleShown(false);
|
|
227
|
+
var t = setTimeout(function () { setHandleShown(true); }, 300); // right after the panel slides away
|
|
228
|
+
return function () { clearTimeout(t); };
|
|
229
|
+
}
|
|
230
|
+
setHandleShown(false);
|
|
231
|
+
}, [open]);
|
|
232
|
+
// panel hover: clear when mouse inside, transparent when outside
|
|
233
|
+
var _s6 = react.useState(false);
|
|
234
|
+
var hovered = _s6[0];
|
|
235
|
+
var setHovered = _s6[1];
|
|
236
|
+
|
|
237
|
+
// panel size (persisted; width default 288, height 0 = auto)
|
|
238
|
+
var PANEL_W_KEY = "dsh-quick-toc.panelW.v1";
|
|
239
|
+
var PANEL_H_KEY = "dsh-quick-toc.panelH.v1";
|
|
240
|
+
var _s7 = react.useState(function () {
|
|
241
|
+
try { var w = Number(localStorage.getItem(PANEL_W_KEY)); if (isFinite(w) && w >= 180) return w; } catch (e) {}
|
|
242
|
+
return PANEL_WIDTH;
|
|
243
|
+
});
|
|
244
|
+
var panelW = _s7[0];
|
|
245
|
+
var setPanelW = _s7[1];
|
|
246
|
+
var _s8 = react.useState(function () {
|
|
247
|
+
try { var h = Number(localStorage.getItem(PANEL_H_KEY)); if (isFinite(h) && h >= 160) return h; } catch (e) {}
|
|
248
|
+
return 0;
|
|
249
|
+
});
|
|
250
|
+
var panelH = _s8[0];
|
|
251
|
+
var setPanelH = _s8[1];
|
|
252
|
+
|
|
253
|
+
// ---- pagination: show the latest N groups; scrolling to the top loads older ----
|
|
254
|
+
var PAGE_SIZE = 6;
|
|
255
|
+
var _s9 = react.useState(PAGE_SIZE);
|
|
256
|
+
var visibleCount = _s9[0];
|
|
257
|
+
var setVisibleCount = _s9[1];
|
|
258
|
+
var listRef = react.useRef(null);
|
|
259
|
+
var didInitScroll = react.useRef(false);
|
|
260
|
+
|
|
261
|
+
// the group currently being read (under the list viewport middle):
|
|
262
|
+
// it stays at full opacity, every other group is dimmed
|
|
263
|
+
var _s10 = react.useState([]);
|
|
264
|
+
var activeGroup = _s10[0];
|
|
265
|
+
var setActiveGroup = _s10[1];
|
|
266
|
+
var activeSigRef = react.useRef("");
|
|
267
|
+
|
|
268
|
+
// first time content appears, scroll the list to the bottom (newest)
|
|
269
|
+
react.useEffect(function () {
|
|
270
|
+
if (didInitScroll.current || !listRef.current || !groups || groups.length === 0) return;
|
|
271
|
+
didInitScroll.current = true;
|
|
272
|
+
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
273
|
+
}, [groups ? groups.length : 0]);
|
|
274
|
+
|
|
275
|
+
// scroll to the top edge loads older groups (keeps the visual position)
|
|
276
|
+
var onListScroll = function (e) {
|
|
277
|
+
var el = e.currentTarget;
|
|
278
|
+
if (el.scrollTop <= 24 && visibleCount < groups.length) {
|
|
279
|
+
var prevH = el.scrollHeight;
|
|
280
|
+
setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
|
|
281
|
+
requestAnimationFrame(function () {
|
|
282
|
+
requestAnimationFrame(function () {
|
|
283
|
+
el.scrollTop += (el.scrollHeight - prevH);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
290
|
+
var onResizeWDown = function (e) {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
e.stopPropagation();
|
|
293
|
+
if (e.button !== 0) return;
|
|
294
|
+
var startX = e.clientX;
|
|
295
|
+
var startW = panelW;
|
|
296
|
+
var lastW = panelW;
|
|
297
|
+
var move = function (ev) {
|
|
298
|
+
var next = Math.max(180, Math.min(560, startW + (ev.clientX - startX)));
|
|
299
|
+
lastW = next;
|
|
300
|
+
setPanelW(next);
|
|
301
|
+
};
|
|
302
|
+
var up = function () {
|
|
303
|
+
window.removeEventListener("pointermove", move);
|
|
304
|
+
window.removeEventListener("pointerup", up);
|
|
305
|
+
try { localStorage.setItem(PANEL_W_KEY, String(lastW)); } catch (e2) {}
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener("pointermove", move);
|
|
308
|
+
window.addEventListener("pointerup", up);
|
|
309
|
+
};
|
|
310
|
+
var onResizeHDown = function (e) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
if (e.button !== 0) return;
|
|
314
|
+
var startY = e.clientY;
|
|
315
|
+
var startH = panelH > 0 ? panelH : 400;
|
|
316
|
+
var lastH = startH;
|
|
317
|
+
var move = function (ev) {
|
|
318
|
+
var next = Math.max(160, Math.min(window.innerHeight - 60, startH + (ev.clientY - startY)));
|
|
319
|
+
lastH = next;
|
|
320
|
+
setPanelH(next);
|
|
321
|
+
};
|
|
322
|
+
var up = function () {
|
|
323
|
+
window.removeEventListener("pointermove", move);
|
|
324
|
+
window.removeEventListener("pointerup", up);
|
|
325
|
+
try { localStorage.setItem(PANEL_H_KEY, String(lastH)); } catch (e2) {}
|
|
326
|
+
};
|
|
327
|
+
window.addEventListener("pointermove", move);
|
|
328
|
+
window.addEventListener("pointerup", up);
|
|
329
|
+
};
|
|
330
|
+
var onResizeCornerDown = function (e) {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
e.stopPropagation();
|
|
333
|
+
if (e.button !== 0) return;
|
|
334
|
+
var startX = e.clientX;
|
|
335
|
+
var startY = e.clientY;
|
|
336
|
+
var startW = panelW;
|
|
337
|
+
var startH = panelH > 0 ? panelH : 400;
|
|
338
|
+
var lastW = startW;
|
|
339
|
+
var lastH = startH;
|
|
340
|
+
var move = function (ev) {
|
|
341
|
+
var w = Math.max(180, Math.min(560, startW + (ev.clientX - startX)));
|
|
342
|
+
var h = Math.max(160, Math.min(window.innerHeight - 60, startH + (ev.clientY - startY)));
|
|
343
|
+
lastW = w;
|
|
344
|
+
lastH = h;
|
|
345
|
+
setPanelW(w);
|
|
346
|
+
setPanelH(h);
|
|
347
|
+
};
|
|
348
|
+
var up = function () {
|
|
349
|
+
window.removeEventListener("pointermove", move);
|
|
350
|
+
window.removeEventListener("pointerup", up);
|
|
351
|
+
try {
|
|
352
|
+
localStorage.setItem(PANEL_W_KEY, String(lastW));
|
|
353
|
+
localStorage.setItem(PANEL_H_KEY, String(lastH));
|
|
354
|
+
} catch (e2) {}
|
|
355
|
+
};
|
|
356
|
+
window.addEventListener("pointermove", move);
|
|
357
|
+
window.addEventListener("pointerup", up);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// ---- node timestamp -> HH:MM (handles ms / s epochs / ISO strings) ----
|
|
361
|
+
var fmtTime = function (t) {
|
|
362
|
+
if (t === undefined || t === null) return "";
|
|
363
|
+
if (typeof t === "string") {
|
|
364
|
+
var d0 = new Date(t);
|
|
365
|
+
if (isNaN(d0.getTime())) return "";
|
|
366
|
+
var hh0 = ("0" + d0.getHours()).slice(-2);
|
|
367
|
+
var mm0 = ("0" + d0.getMinutes()).slice(-2);
|
|
368
|
+
return hh0 + ":" + mm0;
|
|
369
|
+
}
|
|
370
|
+
var n = Number(t);
|
|
371
|
+
if (!isFinite(n) || n <= 0) return "";
|
|
372
|
+
if (n < 1e12) n = n * 1000; // epoch seconds -> ms
|
|
373
|
+
var d = new Date(n);
|
|
374
|
+
if (isNaN(d.getTime())) return "";
|
|
375
|
+
var hh = ("0" + d.getHours()).slice(-2);
|
|
376
|
+
var mm = ("0" + d.getMinutes()).slice(-2);
|
|
377
|
+
return hh + ":" + mm;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// ---- best-effort node time across common field names ----
|
|
381
|
+
var getNodeTime = function (node) {
|
|
382
|
+
var d = node && node.data;
|
|
383
|
+
if (!d) return "";
|
|
384
|
+
var t = d.time !== undefined ? d.time : (d.createdAt !== undefined ? d.createdAt : d.timestamp);
|
|
385
|
+
return fmtTime(t);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// ---- group headings by conversation turn (node.location.turn) ----
|
|
389
|
+
var groups = react.useMemo(function () {
|
|
390
|
+
var result = [];
|
|
391
|
+
var current = null;
|
|
392
|
+
var turnTimes = {};
|
|
393
|
+
if (!order || !nodes) return result;
|
|
394
|
+
// first pass: per-turn time — the LAST message of the turn wins (end time)
|
|
395
|
+
for (var i = 0; i < order.length; i++) {
|
|
396
|
+
var k0 = order[i];
|
|
397
|
+
var n0 = nodes.get(k0);
|
|
398
|
+
if (!n0 || (n0.kind !== "user" && n0.kind !== "assistant-step")) continue;
|
|
399
|
+
var l0 = n0.location;
|
|
400
|
+
var tid0 = l0 && (l0.kind === "turn" || l0.kind === "step") && l0.turn ? l0.turn.turn : null;
|
|
401
|
+
if (tid0 === null) continue;
|
|
402
|
+
var t0 = getNodeTime(n0);
|
|
403
|
+
if (t0) turnTimes[tid0] = t0;
|
|
404
|
+
}
|
|
405
|
+
// second pass: group assistant headings by turn
|
|
406
|
+
for (var j = 0; j < order.length; j++) {
|
|
407
|
+
var key = order[j];
|
|
408
|
+
var node = nodes.get(key);
|
|
409
|
+
if (!node || node.kind !== "assistant-step") continue;
|
|
410
|
+
var loc = node.location;
|
|
411
|
+
var turnId = loc && (loc.kind === "turn" || loc.kind === "step") && loc.turn ? loc.turn.turn : null;
|
|
412
|
+
var sameGroup = current !== null && current.turn === turnId;
|
|
413
|
+
if (!sameGroup) {
|
|
414
|
+
current = { turn: turnId, time: turnId !== null ? (turnTimes[turnId] || "") : "", headings: [] };
|
|
415
|
+
result.push(current);
|
|
416
|
+
}
|
|
417
|
+
var parsed = parseHeadings(extractReplyText(node));
|
|
418
|
+
var hIdx = 0;
|
|
419
|
+
for (var k = 0; k < parsed.length; k++) {
|
|
420
|
+
current.headings.push({ level: parsed[k].level, title: parsed[k].title, key: key, idx: hIdx });
|
|
421
|
+
hIdx++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return result.filter(function (g) { return g.headings.length > 0; });
|
|
425
|
+
}, [order, nodes]);
|
|
426
|
+
var groupTrees = react.useMemo(function () {
|
|
427
|
+
return groups.map(function (g) { return buildTree(g.headings); });
|
|
428
|
+
}, [groups]);
|
|
429
|
+
// pagination slice: the latest `visibleCount` groups
|
|
430
|
+
var shownGroups = groups.slice(Math.max(0, groups.length - visibleCount));
|
|
431
|
+
var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
|
|
432
|
+
|
|
433
|
+
// the group currently being read (the turn under the middle of the
|
|
434
|
+
// CONVERSATION viewport) stays bright in the outline; others are dimmed
|
|
435
|
+
var keyToGroup = react.useMemo(function () {
|
|
436
|
+
var m = {};
|
|
437
|
+
for (var i = 0; i < groups.length; i++) {
|
|
438
|
+
for (var j = 0; j < groups[i].headings.length; j++) {
|
|
439
|
+
m[groups[i].headings[j].key] = i;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return m;
|
|
443
|
+
}, [groups]);
|
|
444
|
+
react.useEffect(function () {
|
|
445
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
446
|
+
if (!sp) return;
|
|
447
|
+
var update = function () {
|
|
448
|
+
var lr = sp.getBoundingClientRect();
|
|
449
|
+
var vTop = lr.top;
|
|
450
|
+
var vBottom = lr.top + lr.height;
|
|
451
|
+
var rows = sp.querySelectorAll("[data-chat-anchor-key]");
|
|
452
|
+
var actives = [];
|
|
453
|
+
var seen = {};
|
|
454
|
+
for (var i = 0; i < rows.length; i++) {
|
|
455
|
+
var r = rows[i].getBoundingClientRect();
|
|
456
|
+
if (r.top < vBottom && r.bottom > vTop) {
|
|
457
|
+
var k = rows[i].dataset.chatAnchorKey;
|
|
458
|
+
var gi = keyToGroup[k];
|
|
459
|
+
if (gi !== undefined && !seen[gi]) { seen[gi] = true; actives.push(gi); }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
var sig = actives.slice().sort().join(",");
|
|
463
|
+
if (sig !== activeSigRef.current) {
|
|
464
|
+
activeSigRef.current = sig;
|
|
465
|
+
setActiveGroup(actives);
|
|
466
|
+
// auto-follow: keep the reading position visible in the outline
|
|
467
|
+
if (actives.length > 0) {
|
|
468
|
+
var gi0 = actives[0];
|
|
469
|
+
// ensure the group's window is loaded (with a small buffer below)
|
|
470
|
+
setVisibleCount(function (prev) {
|
|
471
|
+
var need = groups.length - gi0 + 3;
|
|
472
|
+
return Math.max(prev, Math.min(groups.length, need));
|
|
473
|
+
});
|
|
474
|
+
var el = listRef.current;
|
|
475
|
+
if (el) {
|
|
476
|
+
setTimeout(function () {
|
|
477
|
+
var node = el.querySelector('[data-group-idx="' + gi0 + '"]');
|
|
478
|
+
if (!node) return;
|
|
479
|
+
var er = node.getBoundingClientRect();
|
|
480
|
+
var lr2 = el.getBoundingClientRect();
|
|
481
|
+
if (er.top < lr2.top - 2 || er.bottom > lr2.bottom + 2) {
|
|
482
|
+
el.scrollTo({ top: el.scrollTop + (er.top - lr2.top) - el.clientHeight / 2 + node.offsetHeight / 2, behavior: "smooth" });
|
|
483
|
+
}
|
|
484
|
+
}, 120);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
update();
|
|
490
|
+
sp.addEventListener("scroll", update, { passive: true });
|
|
491
|
+
return function () { sp.removeEventListener("scroll", update); };
|
|
492
|
+
}, [keyToGroup]);
|
|
493
|
+
|
|
494
|
+
// ---- nothing to show without headings ----
|
|
495
|
+
if (groups.length === 0) return null;
|
|
496
|
+
|
|
497
|
+
// ---- geometry: always position by `left` (px) so dock-switch & slide share one animated transition
|
|
498
|
+
var dockRight = dock === "right";
|
|
499
|
+
var panelLeft;
|
|
500
|
+
if (open) {
|
|
501
|
+
panelLeft = dockRight
|
|
502
|
+
? window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW
|
|
503
|
+
: (viewport ? viewport.left + 8 : 8);
|
|
504
|
+
} else {
|
|
505
|
+
panelLeft = dockRight ? window.innerWidth + 24 : -(panelW + 48);
|
|
506
|
+
}
|
|
507
|
+
var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
|
|
508
|
+
var baseTopPx = (viewport ? viewport.top + 14 : window.innerHeight * 0.12) + panelY;
|
|
509
|
+
|
|
510
|
+
// ---- panel top drag bar ----
|
|
511
|
+
var onHandleDown = function (e) {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
e.stopPropagation();
|
|
514
|
+
if (e.button !== 0) return;
|
|
515
|
+
var startY = e.clientY;
|
|
516
|
+
var origY = panelY;
|
|
517
|
+
var move = function (ev) {
|
|
518
|
+
var next = origY + (ev.clientY - startY);
|
|
519
|
+
next = Math.max(-(viewport ? viewport.top : 0), Math.min(window.innerHeight - 90, next));
|
|
520
|
+
setPanelY(next);
|
|
521
|
+
};
|
|
522
|
+
var up = function () {
|
|
523
|
+
window.removeEventListener("pointermove", move);
|
|
524
|
+
window.removeEventListener("pointerup", up);
|
|
525
|
+
try { localStorage.setItem(PANEL_Y_KEY, String(panelY)); } catch (e2) {}
|
|
526
|
+
};
|
|
527
|
+
window.addEventListener("pointermove", move);
|
|
528
|
+
window.addEventListener("pointerup", up);
|
|
529
|
+
};
|
|
530
|
+
var handleBright = function (on) {
|
|
531
|
+
if (handleRef.current) handleRef.current.style.opacity = on ? "1" : "0.35";
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// ---- jump: smooth glide to the exact heading element. When glued to the
|
|
535
|
+
// bottom, lift just past DSH's 25px stick-to-bottom threshold first so
|
|
536
|
+
// the glide is not yanked back. ----
|
|
537
|
+
var jump = function (key, idx) {
|
|
538
|
+
var row = findRow(key);
|
|
539
|
+
if (!row) return;
|
|
540
|
+
var el = row;
|
|
541
|
+
if (idx !== undefined && idx !== null) {
|
|
542
|
+
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
543
|
+
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
544
|
+
}
|
|
545
|
+
var sp = row.closest ? row.closest("[data-conversation-scroll]") : null;
|
|
546
|
+
if (!sp) {
|
|
547
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
var t = el.getBoundingClientRect().top - sp.getBoundingClientRect().top + sp.scrollTop - 20;
|
|
551
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
552
|
+
if (floor - sp.scrollTop <= 25 && Math.abs(t - sp.scrollTop) > 60) {
|
|
553
|
+
sp.scrollTop = Math.max(0, floor - 26);
|
|
554
|
+
}
|
|
555
|
+
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
559
|
+
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
560
|
+
var ox = offset ? (offset.x || 0) : 0;
|
|
561
|
+
var oy = offset ? (offset.y || 0) : 0;
|
|
562
|
+
return react_jsx_runtime.jsx("button", {
|
|
563
|
+
onClick: onClick,
|
|
564
|
+
style: {
|
|
565
|
+
width: 24,
|
|
566
|
+
height: 24,
|
|
567
|
+
display: "flex",
|
|
568
|
+
alignItems: "center",
|
|
569
|
+
justifyContent: "center",
|
|
570
|
+
borderRadius: "50%",
|
|
571
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
572
|
+
border: "none",
|
|
573
|
+
color: C.muted,
|
|
574
|
+
cursor: "pointer",
|
|
575
|
+
fontSize: fontSize || 13,
|
|
576
|
+
lineHeight: "1",
|
|
577
|
+
padding: 0,
|
|
578
|
+
flex: "none",
|
|
579
|
+
transition: "background 0.18s ease, color 0.18s ease"
|
|
580
|
+
},
|
|
581
|
+
title: tip,
|
|
582
|
+
onMouseEnter: function (e) {
|
|
583
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
584
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
585
|
+
},
|
|
586
|
+
onMouseLeave: function (e) {
|
|
587
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
588
|
+
e.currentTarget.style.color = C.muted;
|
|
589
|
+
},
|
|
590
|
+
children: react_jsx_runtime.jsx("span", {
|
|
591
|
+
style: {
|
|
592
|
+
display: "inline-block",
|
|
593
|
+
lineHeight: 1,
|
|
594
|
+
transform: "translate(" + ox + "px," + oy + "px)"
|
|
595
|
+
},
|
|
596
|
+
children: glyph
|
|
597
|
+
})
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// ---- panel ----
|
|
602
|
+
// panel: non-linear slide with fade in/out, slightly slower for a smoother feel
|
|
603
|
+
var panelTransition = "left 0.6s " + EASE + ", opacity 0.45s ease";
|
|
604
|
+
var panelOpacity = open ? (hovered ? 0.95 : 0.45) : 0;
|
|
605
|
+
|
|
606
|
+
var panelEl = react_jsx_runtime.jsx("div", {
|
|
607
|
+
style: {
|
|
608
|
+
position: "fixed",
|
|
609
|
+
top: baseTopPx + "px",
|
|
610
|
+
left: panelLeft + "px",
|
|
611
|
+
width: panelW + "px",
|
|
612
|
+
maxHeight: vMaxH,
|
|
613
|
+
height: panelH > 0 ? panelH + "px" : undefined,
|
|
614
|
+
display: "flex",
|
|
615
|
+
flexDirection: "column",
|
|
616
|
+
background: C.panelBg,
|
|
617
|
+
border: "none",
|
|
618
|
+
borderRadius: 12,
|
|
619
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.18)",
|
|
620
|
+
zIndex: Z_BASE,
|
|
621
|
+
overflow: "hidden",
|
|
622
|
+
color: C.text,
|
|
623
|
+
opacity: panelOpacity,
|
|
624
|
+
transition: panelTransition,
|
|
625
|
+
pointerEvents: "auto"
|
|
626
|
+
},
|
|
627
|
+
onMouseEnter: function () { setHovered(true); },
|
|
628
|
+
onMouseLeave: function () { setHovered(false); },
|
|
629
|
+
onClickCapture: function (e) {
|
|
630
|
+
// jump handled in the capture phase (bubble-phase handlers are unreliable here)
|
|
631
|
+
var t = e.target;
|
|
632
|
+
var item = (t && t.closest) ? t.closest("[data-jump-key]") : null;
|
|
633
|
+
if (item && item.dataset && item.dataset.jumpKey) {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
var idx = item.dataset.jumpIdx !== undefined ? Number(item.dataset.jumpIdx) : 0;
|
|
637
|
+
jump(item.dataset.jumpKey, idx);
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
children: [
|
|
641
|
+
// top block: small grip bar on top + header row below (whole block draggable)
|
|
642
|
+
react_jsx_runtime.jsx("div", {
|
|
643
|
+
style: {
|
|
644
|
+
flex: "none",
|
|
645
|
+
cursor: "grab",
|
|
646
|
+
userSelect: "none"
|
|
647
|
+
},
|
|
648
|
+
title: "按住拖动调整位置",
|
|
649
|
+
onPointerDown: onHandleDown,
|
|
650
|
+
onMouseEnter: function () { handleBright(true); },
|
|
651
|
+
onMouseLeave: function () { handleBright(false); },
|
|
652
|
+
children: [
|
|
653
|
+
react_jsx_runtime.jsx("div", {
|
|
654
|
+
ref: handleRef,
|
|
655
|
+
style: {
|
|
656
|
+
width: 56,
|
|
657
|
+
height: 5,
|
|
658
|
+
borderRadius: 999,
|
|
659
|
+
background: "var(--dsw-alias-border-l2, rgba(128,128,128,0.55))",
|
|
660
|
+
opacity: 0.35,
|
|
661
|
+
transition: "opacity 0.2s ease",
|
|
662
|
+
margin: "5px auto 2px"
|
|
663
|
+
}
|
|
664
|
+
}),
|
|
665
|
+
react_jsx_runtime.jsx("div", {
|
|
666
|
+
style: {
|
|
667
|
+
display: "flex",
|
|
668
|
+
alignItems: "center",
|
|
669
|
+
justifyContent: "space-between",
|
|
670
|
+
padding: "1px 10px 5px",
|
|
671
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
672
|
+
fontSize: 13,
|
|
673
|
+
fontWeight: 600,
|
|
674
|
+
gap: 6
|
|
675
|
+
},
|
|
676
|
+
children: [
|
|
677
|
+
// three-bar outline mark (long / medium / short) — no title text
|
|
678
|
+
react_jsx_runtime.jsx("div", {
|
|
679
|
+
style: { display: "flex", alignItems: "center", color: C.muted, flex: "none" },
|
|
680
|
+
title: "对话大纲",
|
|
681
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
682
|
+
width: 18,
|
|
683
|
+
height: 14,
|
|
684
|
+
viewBox: "0 0 18 14",
|
|
685
|
+
style: { display: "block" },
|
|
686
|
+
children: [
|
|
687
|
+
react_jsx_runtime.jsx("line", { x1: 0, y1: 2, x2: 18, y2: 2, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" }),
|
|
688
|
+
react_jsx_runtime.jsx("line", { x1: 0, y1: 7, x2: 12, y2: 7, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" }),
|
|
689
|
+
react_jsx_runtime.jsx("line", { x1: 0, y1: 12, x2: 6, y2: 12, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" })
|
|
690
|
+
]
|
|
691
|
+
})
|
|
692
|
+
}),
|
|
693
|
+
react_jsx_runtime.jsx("div", {
|
|
694
|
+
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
695
|
+
children: [
|
|
696
|
+
// triangle tips toward the side it will move TO: shift slightly down + toward the tip
|
|
697
|
+
iconBtn(toggleDock, dockRight ? "移到左侧" : "移到右侧", dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: 1 } : { x: 1, y: 1 }),
|
|
698
|
+
iconBtn(function () { setOpen(false); }, "收起", "✕", 13, { x: 0, y: -1 })
|
|
699
|
+
]
|
|
700
|
+
})
|
|
701
|
+
]
|
|
702
|
+
})
|
|
703
|
+
]
|
|
704
|
+
}),
|
|
705
|
+
// outline list (paged: newest first, scroll to the top loads older);
|
|
706
|
+
// the scrollbar follows the dock side (rtl flips it to the left)
|
|
707
|
+
react_jsx_runtime.jsx("div", {
|
|
708
|
+
ref: listRef,
|
|
709
|
+
style: {
|
|
710
|
+
overflowY: "auto",
|
|
711
|
+
padding: "6px 8px",
|
|
712
|
+
maxHeight: "60vh",
|
|
713
|
+
flex: "1 1 auto",
|
|
714
|
+
minHeight: 0,
|
|
715
|
+
direction: dockRight ? "ltr" : "rtl"
|
|
716
|
+
},
|
|
717
|
+
onScroll: onListScroll,
|
|
718
|
+
children: react_jsx_runtime.jsx("div", {
|
|
719
|
+
style: { direction: "ltr" },
|
|
720
|
+
children: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup)
|
|
721
|
+
})
|
|
722
|
+
}),
|
|
723
|
+
// resize handles (right edge: width, bottom edge: height)
|
|
724
|
+
react_jsx_runtime.jsx("div", {
|
|
725
|
+
style: {
|
|
726
|
+
position: "absolute",
|
|
727
|
+
top: 0,
|
|
728
|
+
bottom: 0,
|
|
729
|
+
right: 0,
|
|
730
|
+
width: 8,
|
|
731
|
+
cursor: "col-resize",
|
|
732
|
+
touchAction: "none",
|
|
733
|
+
zIndex: 2
|
|
734
|
+
},
|
|
735
|
+
onPointerDown: onResizeWDown,
|
|
736
|
+
title: "拖拽调整宽度"
|
|
737
|
+
}),
|
|
738
|
+
react_jsx_runtime.jsx("div", {
|
|
739
|
+
style: {
|
|
740
|
+
position: "absolute",
|
|
741
|
+
left: 0,
|
|
742
|
+
right: 0,
|
|
743
|
+
bottom: 0,
|
|
744
|
+
height: 8,
|
|
745
|
+
cursor: "row-resize",
|
|
746
|
+
touchAction: "none",
|
|
747
|
+
zIndex: 2
|
|
748
|
+
},
|
|
749
|
+
onPointerDown: onResizeHDown,
|
|
750
|
+
title: "拖拽调整高度"
|
|
751
|
+
}),
|
|
752
|
+
// corner handle: resize width AND height at once
|
|
753
|
+
react_jsx_runtime.jsx("div", {
|
|
754
|
+
style: {
|
|
755
|
+
position: "absolute",
|
|
756
|
+
right: 0,
|
|
757
|
+
bottom: 0,
|
|
758
|
+
width: 16,
|
|
759
|
+
height: 16,
|
|
760
|
+
cursor: "nwse-resize",
|
|
761
|
+
touchAction: "none",
|
|
762
|
+
zIndex: 3
|
|
763
|
+
},
|
|
764
|
+
onPointerDown: onResizeCornerDown,
|
|
765
|
+
title: "拖拽同时调整宽高"
|
|
766
|
+
})
|
|
767
|
+
]
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// ---- edge collapse handle: fades+pops in after the panel fully slides away ----
|
|
771
|
+
var edgeEl = !open ? react_jsx_runtime.jsx("div", {
|
|
772
|
+
style: {
|
|
773
|
+
position: "fixed",
|
|
774
|
+
width: 26,
|
|
775
|
+
height: 92,
|
|
776
|
+
display: "flex",
|
|
777
|
+
alignItems: "center",
|
|
778
|
+
justifyContent: "center",
|
|
779
|
+
cursor: "pointer",
|
|
780
|
+
zIndex: Z_BASE,
|
|
781
|
+
top: (viewport ? viewport.top + viewport.height / 2 - 46 : "50%"),
|
|
782
|
+
transform: handleShown ? "translateX(0)" : (dockRight ? "translateX(16px)" : "translateX(-16px)"),
|
|
783
|
+
transition: "transform 0.4s " + EASE,
|
|
784
|
+
...(dockRight
|
|
785
|
+
? { right: viewport ? viewport.right + 52 : 60 } // clear of the milestone rail
|
|
786
|
+
: { left: viewport ? viewport.left : 0 })
|
|
787
|
+
},
|
|
788
|
+
title: "展开大纲",
|
|
789
|
+
onClick: function () { setOpen(true); },
|
|
790
|
+
onMouseEnter: function () { if (edgeRef.current) edgeRef.current.style.opacity = "1"; },
|
|
791
|
+
onMouseLeave: function () { if (edgeRef.current) edgeRef.current.style.opacity = "0.5"; },
|
|
792
|
+
children: react_jsx_runtime.jsx("div", {
|
|
793
|
+
ref: edgeRef,
|
|
794
|
+
style: {
|
|
795
|
+
width: 12,
|
|
796
|
+
height: 80,
|
|
797
|
+
borderRadius: 999,
|
|
798
|
+
background: "var(--dsw-alias-border-l2, rgba(160,160,160,0.55))",
|
|
799
|
+
opacity: handleShown ? 0.5 : 0,
|
|
800
|
+
transition: "opacity 0.35s ease, background 0.25s ease",
|
|
801
|
+
display: "flex",
|
|
802
|
+
alignItems: "center",
|
|
803
|
+
justifyContent: "center",
|
|
804
|
+
color: "var(--dsw-alias-label-secondary, #9aa0ab)",
|
|
805
|
+
cursor: "pointer"
|
|
806
|
+
},
|
|
807
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
808
|
+
width: 9,
|
|
809
|
+
height: 24,
|
|
810
|
+
viewBox: "0 0 9 24",
|
|
811
|
+
style: { display: "block" },
|
|
812
|
+
children: react_jsx_runtime.jsx("path", {
|
|
813
|
+
// tip points OUTWARD (toward where the panel will pop out):
|
|
814
|
+
// docked left -> points right; docked right -> points left
|
|
815
|
+
d: dockRight ? "M7 4 L1.5 12 L7 20 Z" : "M2 4 L7.5 12 L2 20 Z",
|
|
816
|
+
fill: "currentColor"
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
}) : null;
|
|
821
|
+
|
|
822
|
+
return react_jsx_runtime.jsx(ErrorBoundary, {
|
|
823
|
+
children: [panelEl, edgeEl]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function renderItem(n, depth, jump, C, uid) {
|
|
828
|
+
return react_jsx_runtime.jsx(
|
|
829
|
+
"div",
|
|
830
|
+
{
|
|
831
|
+
onClick: function () { jump(n.key, n.idx); },
|
|
832
|
+
"data-jump-key": n.key,
|
|
833
|
+
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
834
|
+
style: {
|
|
835
|
+
padding: "2px 6px",
|
|
836
|
+
paddingLeft: 8 + (n.level - 1) * 12,
|
|
837
|
+
margin: "1px 0",
|
|
838
|
+
borderRadius: 6,
|
|
839
|
+
cursor: "pointer",
|
|
840
|
+
fontSize: n.level <= 2 ? 13 : 12,
|
|
841
|
+
color: n.level <= 2 ? C.text : C.muted,
|
|
842
|
+
fontWeight: n.level <= 2 ? 600 : 400,
|
|
843
|
+
lineHeight: "18px",
|
|
844
|
+
height: 22,
|
|
845
|
+
whiteSpace: "nowrap",
|
|
846
|
+
overflow: "hidden",
|
|
847
|
+
textOverflow: "ellipsis"
|
|
848
|
+
},
|
|
849
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
850
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
851
|
+
title: n.title,
|
|
852
|
+
children: n.title
|
|
853
|
+
},
|
|
854
|
+
uid + "-" + n.level + "-" + (n.key || "")
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function renderNodes(nodes, depth, jump, C, uid) {
|
|
859
|
+
var out = [];
|
|
860
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
861
|
+
var n = nodes[i];
|
|
862
|
+
out.push(renderItem(n, depth, jump, C, uid + "-" + i));
|
|
863
|
+
if (n.children && n.children.length) {
|
|
864
|
+
out.push(react_jsx_runtime.jsx("div", { children: renderNodes(n.children, depth + 1, jump, C, uid + "-" + i) }, uid + "-c-" + i));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// render each conversation turn as its own block: solid divider + time header
|
|
871
|
+
// offset = global group index of the first rendered group (stable React keys)
|
|
872
|
+
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
873
|
+
function renderGroups(groups, trees, jump, C, offset, activeIdx) {
|
|
874
|
+
var base = offset || 0;
|
|
875
|
+
var activeSet = {};
|
|
876
|
+
if (Array.isArray(activeIdx)) {
|
|
877
|
+
for (var a = 0; a < activeIdx.length; a++) activeSet[activeIdx[a]] = true;
|
|
878
|
+
}
|
|
879
|
+
var out = [];
|
|
880
|
+
for (var i = 0; i < groups.length; i++) {
|
|
881
|
+
var g = groups[i];
|
|
882
|
+
var gi = base + i;
|
|
883
|
+
var items = [];
|
|
884
|
+
items.push(react_jsx_runtime.jsx("div", {
|
|
885
|
+
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
886
|
+
}, "g-sep-" + gi));
|
|
887
|
+
if (g.time) {
|
|
888
|
+
items.push(react_jsx_runtime.jsx("div", {
|
|
889
|
+
style: { padding: "1px 4px 2px", fontSize: 11, color: C.muted, fontWeight: 600, lineHeight: "15px", height: 18 },
|
|
890
|
+
children: g.time
|
|
891
|
+
}, "g-h-" + gi));
|
|
892
|
+
} else {
|
|
893
|
+
items.push(react_jsx_runtime.jsx("div", { style: { height: 18 }, children: null }, "g-h-" + gi));
|
|
894
|
+
}
|
|
895
|
+
items.push(renderNodes(trees[i], 0, jump, C, "g" + gi));
|
|
896
|
+
var dim = !activeSet[gi];
|
|
897
|
+
out.push(react_jsx_runtime.jsx("div", {
|
|
898
|
+
"data-group-idx": gi,
|
|
899
|
+
style: { opacity: dim ? 0.55 : 1, transition: "opacity 0.3s ease" },
|
|
900
|
+
children: items
|
|
901
|
+
}, "g-" + gi));
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function OutlineOverlay(props) {
|
|
907
|
+
var SessionProvider = props.SessionProvider;
|
|
908
|
+
var renderSlot = props.renderSlot;
|
|
909
|
+
return react_jsx_runtime.jsx(SessionProvider, {
|
|
910
|
+
empty: function () { return null; },
|
|
911
|
+
children: function () { return renderSlot("quick-toc.panel", {}); }
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
var zh = {
|
|
916
|
+
"panel.title": "对话大纲"
|
|
917
|
+
};
|
|
918
|
+
var en = {
|
|
919
|
+
"panel.title": "Conversation Outline"
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
var inject = ["slots", "sessions", "locale"];
|
|
923
|
+
|
|
924
|
+
function apply(ctx) {
|
|
925
|
+
ctx.effect(function () {
|
|
926
|
+
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
927
|
+
}, "dsh-quick-toc: dictionaries");
|
|
928
|
+
ctx.slots.inject("shell.overlay", function () {
|
|
929
|
+
return ctx.slots.register({
|
|
930
|
+
name: "shell.overlay",
|
|
931
|
+
id: "quick-toc",
|
|
932
|
+
order: 90,
|
|
933
|
+
children: { "quick-toc.panel": { kind: "single", scope: "session" } }
|
|
934
|
+
}, OutlineOverlay);
|
|
935
|
+
});
|
|
936
|
+
ctx.slots.inject("quick-toc.panel", function () {
|
|
937
|
+
return ctx.slots.register({
|
|
938
|
+
name: "quick-toc.panel",
|
|
939
|
+
locale: "dsh-quick-toc"
|
|
940
|
+
}, OutlinePanel);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
exports.apply = apply;
|
|
945
|
+
exports.inject = inject;
|
|
946
|
+
return module.exports;
|
|
947
|
+
}
|
|
948
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-outline node half. Pure UI plugin: the empty apply exists so the
|
|
3
|
+
* plugin appears in the host loader (load and lifecycle follow the host);
|
|
4
|
+
* the browser half ships via exports["./client"], discovered through the
|
|
5
|
+
* package.json dsh.client declaration.
|
|
6
|
+
*/
|
|
7
|
+
function apply() {}
|
|
8
|
+
export { apply };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-quick-toc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Quick conversation TOC for DeepSeek Harness: markdown heading outline grouped by turn, with auto-follow highlight and smooth jump navigation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"cordis.patch.yml",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./lib/index.js",
|
|
15
|
+
"./client": "./lib/client.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"inject": [
|
|
24
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
27
|
+
],
|
|
28
|
+
"platform": "web"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"dsh",
|
|
34
|
+
"deepseek-harness",
|
|
35
|
+
"plugin",
|
|
36
|
+
"toc",
|
|
37
|
+
"outline",
|
|
38
|
+
"markdown"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/LyaxZ/dsh-quick-toc.git"
|
|
43
|
+
}
|
|
44
|
+
}
|