dsh-quick-toc 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -1
- package/README.md +7 -1
- package/lib/client.js +215 -27
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -17,7 +17,13 @@ A quick conversation TOC plugin for [DeepSeek Harness](https://github.com/deepse
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
With the DSH CLI:
|
|
20
|
+
With the DSH CLI (published on npm — name only):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dsh plugin --profile web add dsh-quick-toc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or from GitHub:
|
|
21
27
|
|
|
22
28
|
```
|
|
23
29
|
dsh plugin --profile web add github:LyaxZ/dsh-quick-toc
|
package/README.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -42,6 +42,34 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
.trim();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// user messages carry their text in data.content (assistant replies use
|
|
46
|
+
// data.blocks) — read whichever is present
|
|
47
|
+
function extractUserText(node) {
|
|
48
|
+
var d = node && node.data;
|
|
49
|
+
if (!d) return "";
|
|
50
|
+
var content = d.content;
|
|
51
|
+
if (typeof content === "string") return content;
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
var out = "";
|
|
54
|
+
for (var i = 0; i < content.length; i++) {
|
|
55
|
+
var b = content[i];
|
|
56
|
+
if (b && typeof b === "object" && (b.type === "text" || b.kind === "text") && typeof b.text === "string") {
|
|
57
|
+
out += b.text + "\n";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
return extractReplyText(node);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// first non-empty line of a message, truncated for the outline header
|
|
66
|
+
function previewText(text, max) {
|
|
67
|
+
if (!text) return "";
|
|
68
|
+
var first = text.split("\n").map(function (s) { return s.trim(); }).filter(Boolean)[0] || "";
|
|
69
|
+
if (first.length > max) first = first.slice(0, max) + "…";
|
|
70
|
+
return first;
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
function parseHeadings(text) {
|
|
46
74
|
var items = [];
|
|
47
75
|
var re = /^(#{1,6})\s+(.+?)\s*#*\s*$/gm;
|
|
@@ -65,6 +93,28 @@ window.__ModuleLoader__.load({
|
|
|
65
93
|
return root.children;
|
|
66
94
|
}
|
|
67
95
|
|
|
96
|
+
// strict lookup: exact dataset match or CSS-escaped selector only —
|
|
97
|
+
// no fuzzy contains matching (avoids landing on the wrong row);
|
|
98
|
+
// hidden rows (zero rect, e.g. duplicate/hidden copies) are skipped
|
|
99
|
+
function findRowStrict(key) {
|
|
100
|
+
if (!key) return null;
|
|
101
|
+
var rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
102
|
+
for (var i = 0; i < rows.length; i++) {
|
|
103
|
+
if (rows[i].dataset && rows[i].dataset.chatAnchorKey === key) {
|
|
104
|
+
var r = rows[i].getBoundingClientRect();
|
|
105
|
+
if (r.width > 0 && r.height > 0) return rows[i];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
var el = document.querySelector('[data-chat-anchor-key="' + window.CSS.escape(key) + '"]');
|
|
110
|
+
if (el) {
|
|
111
|
+
var r2 = el.getBoundingClientRect();
|
|
112
|
+
if (r2.width > 0 && r2.height > 0) return el;
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
68
118
|
function findRow(key) {
|
|
69
119
|
if (!key) return null;
|
|
70
120
|
var rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
@@ -85,6 +135,19 @@ window.__ModuleLoader__.load({
|
|
|
85
135
|
return null;
|
|
86
136
|
}
|
|
87
137
|
|
|
138
|
+
// find the conversation's "load older messages" button (scoped to the
|
|
139
|
+
// conversation scrollport so panel buttons are never matched)
|
|
140
|
+
function findLoadOlderButton() {
|
|
141
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
142
|
+
if (!sp) return null;
|
|
143
|
+
var buttons = sp.querySelectorAll("button");
|
|
144
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
145
|
+
var t = (buttons[i].textContent || "").trim();
|
|
146
|
+
if (/加载|更早|loadOlder|older/i.test(t)) return buttons[i];
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
88
151
|
// ---------- theme colors: DSH CSS variables (adapt to light/dark) ----------
|
|
89
152
|
var C = {
|
|
90
153
|
panelBg: "var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96))",
|
|
@@ -257,6 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
257
320
|
var setVisibleCount = _s9[1];
|
|
258
321
|
var listRef = react.useRef(null);
|
|
259
322
|
var didInitScroll = react.useRef(false);
|
|
323
|
+
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
260
324
|
|
|
261
325
|
// the group currently being read (under the list viewport middle):
|
|
262
326
|
// it stays at full opacity, every other group is dimmed
|
|
@@ -272,20 +336,58 @@ window.__ModuleLoader__.load({
|
|
|
272
336
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
273
337
|
}, [groups ? groups.length : 0]);
|
|
274
338
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
339
|
+
// load older outline groups; when everything is loaded, click the
|
|
340
|
+
// conversation's own "load older" button so older turns keep appearing
|
|
341
|
+
var loadOlderOutline = function (el) {
|
|
342
|
+
var grow = function () {
|
|
343
|
+
if (visibleCount < groups.length) {
|
|
344
|
+
var prevH = el.scrollHeight;
|
|
345
|
+
setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
|
|
282
346
|
requestAnimationFrame(function () {
|
|
283
|
-
|
|
347
|
+
requestAnimationFrame(function () {
|
|
348
|
+
el.scrollTop += (el.scrollHeight - prevH);
|
|
349
|
+
});
|
|
284
350
|
});
|
|
285
|
-
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
};
|
|
355
|
+
if (grow()) return;
|
|
356
|
+
var btn = findLoadOlderButton();
|
|
357
|
+
if (btn && !btn.disabled) {
|
|
358
|
+
btn.click();
|
|
359
|
+
// once the conversation loads more, expand the outline window too
|
|
360
|
+
setTimeout(function () {
|
|
361
|
+
var prevH2 = el.scrollHeight;
|
|
362
|
+
if (grow()) {
|
|
363
|
+
requestAnimationFrame(function () {
|
|
364
|
+
requestAnimationFrame(function () {
|
|
365
|
+
el.scrollTop += (el.scrollHeight - prevH2);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}, 1200);
|
|
286
370
|
}
|
|
287
371
|
};
|
|
288
372
|
|
|
373
|
+
// wheel up (toward older) loads more when the list is at its top or has
|
|
374
|
+
// nothing to scroll (content shorter than the panel) — so scrolling up
|
|
375
|
+
// always refreshes older turns, even without a visible scrollbar
|
|
376
|
+
var onListWheel = function (e) {
|
|
377
|
+
outlineTouchRef.current = Date.now();
|
|
378
|
+
if (e.deltaY >= 0) return;
|
|
379
|
+
var el = listRef.current;
|
|
380
|
+
if (!el) return;
|
|
381
|
+
if (el.scrollTop <= 1) loadOlderOutline(el);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
385
|
+
var onListScroll = function (e) {
|
|
386
|
+
outlineTouchRef.current = Date.now();
|
|
387
|
+
var el = e.currentTarget;
|
|
388
|
+
if (el.scrollTop <= 24) loadOlderOutline(el);
|
|
389
|
+
};
|
|
390
|
+
|
|
289
391
|
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
290
392
|
var onResizeWDown = function (e) {
|
|
291
393
|
e.preventDefault();
|
|
@@ -390,8 +492,11 @@ window.__ModuleLoader__.load({
|
|
|
390
492
|
var result = [];
|
|
391
493
|
var current = null;
|
|
392
494
|
var turnTimes = {};
|
|
495
|
+
var turnUserKey = {};
|
|
496
|
+
var turnUserText = {};
|
|
393
497
|
if (!order || !nodes) return result;
|
|
394
|
-
// first pass: per-turn time — the LAST message of the turn wins (end time)
|
|
498
|
+
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
499
|
+
// also remember each turn's user message key + first-line preview
|
|
395
500
|
for (var i = 0; i < order.length; i++) {
|
|
396
501
|
var k0 = order[i];
|
|
397
502
|
var n0 = nodes.get(k0);
|
|
@@ -399,6 +504,10 @@ window.__ModuleLoader__.load({
|
|
|
399
504
|
var l0 = n0.location;
|
|
400
505
|
var tid0 = l0 && (l0.kind === "turn" || l0.kind === "step") && l0.turn ? l0.turn.turn : null;
|
|
401
506
|
if (tid0 === null) continue;
|
|
507
|
+
if (n0.kind === "user" && turnUserKey[tid0] === undefined) {
|
|
508
|
+
turnUserKey[tid0] = k0;
|
|
509
|
+
turnUserText[tid0] = previewText(extractUserText(n0), 30);
|
|
510
|
+
}
|
|
402
511
|
var t0 = getNodeTime(n0);
|
|
403
512
|
if (t0) turnTimes[tid0] = t0;
|
|
404
513
|
}
|
|
@@ -411,7 +520,13 @@ window.__ModuleLoader__.load({
|
|
|
411
520
|
var turnId = loc && (loc.kind === "turn" || loc.kind === "step") && loc.turn ? loc.turn.turn : null;
|
|
412
521
|
var sameGroup = current !== null && current.turn === turnId;
|
|
413
522
|
if (!sameGroup) {
|
|
414
|
-
current = {
|
|
523
|
+
current = {
|
|
524
|
+
turn: turnId,
|
|
525
|
+
time: turnId !== null ? (turnTimes[turnId] || "") : "",
|
|
526
|
+
userKey: turnId !== null ? (turnUserKey[turnId] || "") : "",
|
|
527
|
+
userText: turnId !== null ? (turnUserText[turnId] || "") : "",
|
|
528
|
+
headings: []
|
|
529
|
+
};
|
|
415
530
|
result.push(current);
|
|
416
531
|
}
|
|
417
532
|
var parsed = parseHeadings(extractReplyText(node));
|
|
@@ -421,7 +536,9 @@ window.__ModuleLoader__.load({
|
|
|
421
536
|
hIdx++;
|
|
422
537
|
}
|
|
423
538
|
}
|
|
424
|
-
|
|
539
|
+
// keep a turn when it has headings OR a time — turns without headings
|
|
540
|
+
// still get a standalone time entry in the outline (click to jump)
|
|
541
|
+
return result.filter(function (g) { return g.headings.length > 0 || g.time !== ""; });
|
|
425
542
|
}, [order, nodes]);
|
|
426
543
|
var groupTrees = react.useMemo(function () {
|
|
427
544
|
return groups.map(function (g) { return buildTree(g.headings); });
|
|
@@ -463,8 +580,10 @@ window.__ModuleLoader__.load({
|
|
|
463
580
|
if (sig !== activeSigRef.current) {
|
|
464
581
|
activeSigRef.current = sig;
|
|
465
582
|
setActiveGroup(actives);
|
|
466
|
-
// auto-follow: keep the reading position visible in the outline
|
|
467
|
-
|
|
583
|
+
// auto-follow: keep the reading position visible in the outline —
|
|
584
|
+
// but pause for ~2s after the user touches the outline themselves,
|
|
585
|
+
// otherwise loading older turns gets yanked back to the bottom
|
|
586
|
+
if (actives.length > 0 && Date.now() - outlineTouchRef.current > 2000) {
|
|
468
587
|
var gi0 = actives[0];
|
|
469
588
|
// ensure the group's window is loaded (with a small buffer below)
|
|
470
589
|
setVisibleCount(function (prev) {
|
|
@@ -502,7 +621,19 @@ window.__ModuleLoader__.load({
|
|
|
502
621
|
? window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW
|
|
503
622
|
: (viewport ? viewport.left + 8 : 8);
|
|
504
623
|
} else {
|
|
505
|
-
|
|
624
|
+
// collapsed position:
|
|
625
|
+
// left dock -> slide just past the left sidebar edge (stop there)
|
|
626
|
+
// right dock -> boundary is the right sidebar when it is open,
|
|
627
|
+
// otherwise the screen's right edge
|
|
628
|
+
if (dockRight) {
|
|
629
|
+
// with the right sidebar open, slide INTO the sidebar area (covered
|
|
630
|
+
// by the cover wall); otherwise fly off the screen's right edge
|
|
631
|
+
panelLeft = (viewport && viewport.right > 80)
|
|
632
|
+
? (window.innerWidth - viewport.right)
|
|
633
|
+
: window.innerWidth + 24;
|
|
634
|
+
} else {
|
|
635
|
+
panelLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
|
|
636
|
+
}
|
|
506
637
|
}
|
|
507
638
|
var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
|
|
508
639
|
var baseTopPx = (viewport ? viewport.top + 14 : window.innerHeight * 0.12) + panelY;
|
|
@@ -599,8 +730,26 @@ window.__ModuleLoader__.load({
|
|
|
599
730
|
};
|
|
600
731
|
|
|
601
732
|
// ---- panel ----
|
|
602
|
-
// panel:
|
|
603
|
-
|
|
733
|
+
// panel: opening slides in with a slow fade; closing slides quickly to
|
|
734
|
+
// the dock edge, clipped by the sidebar line (looks covered, not
|
|
735
|
+
// dissolving) and only fades at the very end. No box-shadow: a shadow
|
|
736
|
+
// would get cut in half by the clip-path, so the panel is flat.
|
|
737
|
+
var panelTransition = open
|
|
738
|
+
? "left 0.6s " + EASE + ", clip-path 0.6s " + EASE + ", opacity 0.45s ease"
|
|
739
|
+
: "left 0.28s " + EASE + ", clip-path 0.28s " + EASE + ", opacity 0.14s ease 0.26s";
|
|
740
|
+
|
|
741
|
+
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
742
|
+
// like being covered by the sidebar (the sidebar stays untouched):
|
|
743
|
+
// left dock -> clipped from the left up to the sidebar line
|
|
744
|
+
// right dock -> clipped from the right at the screen/sidebar line
|
|
745
|
+
var panelClip = "inset(0 0 0 0px)";
|
|
746
|
+
if (!open) {
|
|
747
|
+
if (dockRight) {
|
|
748
|
+
panelClip = "inset(0 " + panelW + "px 0 0)";
|
|
749
|
+
} else {
|
|
750
|
+
panelClip = "inset(0 0 0 " + (panelW + 8) + "px)";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
604
753
|
var panelOpacity = open ? (hovered ? 0.95 : 0.45) : 0;
|
|
605
754
|
|
|
606
755
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
@@ -621,6 +770,8 @@ window.__ModuleLoader__.load({
|
|
|
621
770
|
overflow: "hidden",
|
|
622
771
|
color: C.text,
|
|
623
772
|
opacity: panelOpacity,
|
|
773
|
+
clipPath: panelClip,
|
|
774
|
+
boxShadow: "none",
|
|
624
775
|
transition: panelTransition,
|
|
625
776
|
pointerEvents: "auto"
|
|
626
777
|
},
|
|
@@ -709,12 +860,12 @@ window.__ModuleLoader__.load({
|
|
|
709
860
|
style: {
|
|
710
861
|
overflowY: "auto",
|
|
711
862
|
padding: "6px 8px",
|
|
712
|
-
maxHeight: "60vh",
|
|
713
863
|
flex: "1 1 auto",
|
|
714
864
|
minHeight: 0,
|
|
715
865
|
direction: dockRight ? "ltr" : "rtl"
|
|
716
866
|
},
|
|
717
867
|
onScroll: onListScroll,
|
|
868
|
+
onWheel: onListWheel,
|
|
718
869
|
children: react_jsx_runtime.jsx("div", {
|
|
719
870
|
style: { direction: "ltr" },
|
|
720
871
|
children: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup)
|
|
@@ -867,6 +1018,50 @@ window.__ModuleLoader__.load({
|
|
|
867
1018
|
return out;
|
|
868
1019
|
}
|
|
869
1020
|
|
|
1021
|
+
// a group's header: clicking the time jumps to that turn's start.
|
|
1022
|
+
// Implemented as a function (not an inline closure in a loop) so each
|
|
1023
|
+
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
1024
|
+
// otherwise make every header jump to the last group.
|
|
1025
|
+
function renderGroupHeader(g, gi, jump, C) {
|
|
1026
|
+
var jumpToTurn = function (e) {
|
|
1027
|
+
e.stopPropagation();
|
|
1028
|
+
if (!g.userKey) return;
|
|
1029
|
+
var r0 = findRowStrict(g.userKey);
|
|
1030
|
+
if (r0) jump(g.userKey);
|
|
1031
|
+
else if (g.headings.length > 0) jump(g.headings[0].key, g.headings[0].idx);
|
|
1032
|
+
};
|
|
1033
|
+
return react_jsx_runtime.jsx("div", {
|
|
1034
|
+
style: { padding: "1px 4px 2px", height: 18, display: "flex", alignItems: "center", minWidth: 0 },
|
|
1035
|
+
children: react_jsx_runtime.jsx("span", {
|
|
1036
|
+
onClick: jumpToTurn,
|
|
1037
|
+
title: g.userKey ? "跳转到该回合开头" : "",
|
|
1038
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
1039
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
1040
|
+
style: {
|
|
1041
|
+
fontSize: 11,
|
|
1042
|
+
color: C.muted,
|
|
1043
|
+
cursor: "pointer",
|
|
1044
|
+
padding: "1px 5px",
|
|
1045
|
+
borderRadius: 4,
|
|
1046
|
+
transition: "background 0.15s ease",
|
|
1047
|
+
display: "inline-flex",
|
|
1048
|
+
alignItems: "center",
|
|
1049
|
+
gap: 5,
|
|
1050
|
+
minWidth: 0,
|
|
1051
|
+
maxWidth: "100%",
|
|
1052
|
+
overflow: "hidden"
|
|
1053
|
+
},
|
|
1054
|
+
children: [
|
|
1055
|
+
react_jsx_runtime.jsx("span", { style: { fontWeight: 600, flex: "none" }, children: g.time || " " }),
|
|
1056
|
+
g.userText ? react_jsx_runtime.jsx("span", {
|
|
1057
|
+
style: { fontWeight: 400, opacity: 0.75, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
|
|
1058
|
+
children: g.userText
|
|
1059
|
+
}) : null
|
|
1060
|
+
]
|
|
1061
|
+
})
|
|
1062
|
+
}, "g-h-" + gi);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
870
1065
|
// render each conversation turn as its own block: solid divider + time header
|
|
871
1066
|
// offset = global group index of the first rendered group (stable React keys)
|
|
872
1067
|
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
@@ -884,19 +1079,12 @@ window.__ModuleLoader__.load({
|
|
|
884
1079
|
items.push(react_jsx_runtime.jsx("div", {
|
|
885
1080
|
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
886
1081
|
}, "g-sep-" + gi));
|
|
887
|
-
|
|
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
|
-
}
|
|
1082
|
+
items.push(renderGroupHeader(g, gi, jump, C));
|
|
895
1083
|
items.push(renderNodes(trees[i], 0, jump, C, "g" + gi));
|
|
896
1084
|
var dim = !activeSet[gi];
|
|
897
1085
|
out.push(react_jsx_runtime.jsx("div", {
|
|
898
1086
|
"data-group-idx": gi,
|
|
899
|
-
style: { opacity: dim ? 0.
|
|
1087
|
+
style: { opacity: dim ? 0.6 : 1, transition: "opacity 0.3s ease" },
|
|
900
1088
|
children: items
|
|
901
1089
|
}, "g-" + gi));
|
|
902
1090
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Quick conversation TOC for DeepSeek Harness: markdown heading outline grouped by turn, with auto-follow highlight and smooth jump navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
],
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/LyaxZ/dsh-quick-toc.git"
|
|
42
|
+
"url": "git+https://github.com/LyaxZ/dsh-quick-toc.git"
|
|
43
43
|
}
|
|
44
44
|
}
|