dsh-quick-toc 0.1.0 → 0.2.0-beta
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 +464 -30
- 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,58 @@ 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
|
|
324
|
+
|
|
325
|
+
// ---- search: header button opens a keyword box; Enter cycles through
|
|
326
|
+
// matching headings and jumps to each ----
|
|
327
|
+
var _s13 = react.useState(false);
|
|
328
|
+
var searchOpen = _s13[0];
|
|
329
|
+
var setSearchOpen = _s13[1];
|
|
330
|
+
var _s14 = react.useState("");
|
|
331
|
+
var query = _s14[0];
|
|
332
|
+
var setQuery = _s14[1];
|
|
333
|
+
var _s15 = react.useState(0);
|
|
334
|
+
var matchIdx = _s15[0];
|
|
335
|
+
var setMatchIdx = _s15[1];
|
|
336
|
+
var searchRef = react.useRef(null);
|
|
337
|
+
// animation state: hidden | enter | shown | out; and a transient
|
|
338
|
+
// "searching" flag that drives the spinner
|
|
339
|
+
var _s16 = react.useState("hidden");
|
|
340
|
+
var searchAnim = _s16[0];
|
|
341
|
+
var setSearchAnim = _s16[1];
|
|
342
|
+
var _s17 = react.useState(false);
|
|
343
|
+
var searching = _s17[0];
|
|
344
|
+
var setSearching = _s17[1];
|
|
345
|
+
var searchTimerRef = react.useRef(null);
|
|
346
|
+
var openSearch = function () {
|
|
347
|
+
setSearchOpen(true);
|
|
348
|
+
setSearchAnim("enter");
|
|
349
|
+
};
|
|
350
|
+
var closeSearch = function () {
|
|
351
|
+
setSearchAnim("out");
|
|
352
|
+
setQuery("");
|
|
353
|
+
setTimeout(function () {
|
|
354
|
+
setSearchOpen(false);
|
|
355
|
+
setSearchAnim("hidden");
|
|
356
|
+
}, 240);
|
|
357
|
+
};
|
|
358
|
+
react.useEffect(function () {
|
|
359
|
+
if (searchAnim === "enter") {
|
|
360
|
+
var raf = requestAnimationFrame(function () { setSearchAnim("shown"); });
|
|
361
|
+
return function () { cancelAnimationFrame(raf); };
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}, [searchAnim]);
|
|
365
|
+
react.useEffect(function () {
|
|
366
|
+
if (searchOpen && searchRef.current) searchRef.current.focus();
|
|
367
|
+
}, [searchOpen]);
|
|
368
|
+
var onQueryChange = function (v) {
|
|
369
|
+
setQuery(v);
|
|
370
|
+
setMatchIdx(0);
|
|
371
|
+
setSearching(true);
|
|
372
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
373
|
+
searchTimerRef.current = setTimeout(function () { setSearching(false); }, 220);
|
|
374
|
+
};
|
|
260
375
|
|
|
261
376
|
// the group currently being read (under the list viewport middle):
|
|
262
377
|
// it stays at full opacity, every other group is dimmed
|
|
@@ -272,20 +387,58 @@ window.__ModuleLoader__.load({
|
|
|
272
387
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
273
388
|
}, [groups ? groups.length : 0]);
|
|
274
389
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
390
|
+
// load older outline groups; when everything is loaded, click the
|
|
391
|
+
// conversation's own "load older" button so older turns keep appearing
|
|
392
|
+
var loadOlderOutline = function (el) {
|
|
393
|
+
var grow = function () {
|
|
394
|
+
if (visibleCount < groups.length) {
|
|
395
|
+
var prevH = el.scrollHeight;
|
|
396
|
+
setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
|
|
282
397
|
requestAnimationFrame(function () {
|
|
283
|
-
|
|
398
|
+
requestAnimationFrame(function () {
|
|
399
|
+
el.scrollTop += (el.scrollHeight - prevH);
|
|
400
|
+
});
|
|
284
401
|
});
|
|
285
|
-
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
};
|
|
406
|
+
if (grow()) return;
|
|
407
|
+
var btn = findLoadOlderButton();
|
|
408
|
+
if (btn && !btn.disabled) {
|
|
409
|
+
btn.click();
|
|
410
|
+
// once the conversation loads more, expand the outline window too
|
|
411
|
+
setTimeout(function () {
|
|
412
|
+
var prevH2 = el.scrollHeight;
|
|
413
|
+
if (grow()) {
|
|
414
|
+
requestAnimationFrame(function () {
|
|
415
|
+
requestAnimationFrame(function () {
|
|
416
|
+
el.scrollTop += (el.scrollHeight - prevH2);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}, 1200);
|
|
286
421
|
}
|
|
287
422
|
};
|
|
288
423
|
|
|
424
|
+
// wheel up (toward older) loads more when the list is at its top or has
|
|
425
|
+
// nothing to scroll (content shorter than the panel) — so scrolling up
|
|
426
|
+
// always refreshes older turns, even without a visible scrollbar
|
|
427
|
+
var onListWheel = function (e) {
|
|
428
|
+
outlineTouchRef.current = Date.now();
|
|
429
|
+
if (e.deltaY >= 0) return;
|
|
430
|
+
var el = listRef.current;
|
|
431
|
+
if (!el) return;
|
|
432
|
+
if (el.scrollTop <= 1) loadOlderOutline(el);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
436
|
+
var onListScroll = function (e) {
|
|
437
|
+
outlineTouchRef.current = Date.now();
|
|
438
|
+
var el = e.currentTarget;
|
|
439
|
+
if (el.scrollTop <= 24) loadOlderOutline(el);
|
|
440
|
+
};
|
|
441
|
+
|
|
289
442
|
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
290
443
|
var onResizeWDown = function (e) {
|
|
291
444
|
e.preventDefault();
|
|
@@ -390,8 +543,11 @@ window.__ModuleLoader__.load({
|
|
|
390
543
|
var result = [];
|
|
391
544
|
var current = null;
|
|
392
545
|
var turnTimes = {};
|
|
546
|
+
var turnUserKey = {};
|
|
547
|
+
var turnUserText = {};
|
|
393
548
|
if (!order || !nodes) return result;
|
|
394
|
-
// first pass: per-turn time — the LAST message of the turn wins (end time)
|
|
549
|
+
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
550
|
+
// also remember each turn's user message key + first-line preview
|
|
395
551
|
for (var i = 0; i < order.length; i++) {
|
|
396
552
|
var k0 = order[i];
|
|
397
553
|
var n0 = nodes.get(k0);
|
|
@@ -399,6 +555,10 @@ window.__ModuleLoader__.load({
|
|
|
399
555
|
var l0 = n0.location;
|
|
400
556
|
var tid0 = l0 && (l0.kind === "turn" || l0.kind === "step") && l0.turn ? l0.turn.turn : null;
|
|
401
557
|
if (tid0 === null) continue;
|
|
558
|
+
if (n0.kind === "user" && turnUserKey[tid0] === undefined) {
|
|
559
|
+
turnUserKey[tid0] = k0;
|
|
560
|
+
turnUserText[tid0] = previewText(extractUserText(n0), 30);
|
|
561
|
+
}
|
|
402
562
|
var t0 = getNodeTime(n0);
|
|
403
563
|
if (t0) turnTimes[tid0] = t0;
|
|
404
564
|
}
|
|
@@ -411,7 +571,13 @@ window.__ModuleLoader__.load({
|
|
|
411
571
|
var turnId = loc && (loc.kind === "turn" || loc.kind === "step") && loc.turn ? loc.turn.turn : null;
|
|
412
572
|
var sameGroup = current !== null && current.turn === turnId;
|
|
413
573
|
if (!sameGroup) {
|
|
414
|
-
current = {
|
|
574
|
+
current = {
|
|
575
|
+
turn: turnId,
|
|
576
|
+
time: turnId !== null ? (turnTimes[turnId] || "") : "",
|
|
577
|
+
userKey: turnId !== null ? (turnUserKey[turnId] || "") : "",
|
|
578
|
+
userText: turnId !== null ? (turnUserText[turnId] || "") : "",
|
|
579
|
+
headings: []
|
|
580
|
+
};
|
|
415
581
|
result.push(current);
|
|
416
582
|
}
|
|
417
583
|
var parsed = parseHeadings(extractReplyText(node));
|
|
@@ -421,7 +587,9 @@ window.__ModuleLoader__.load({
|
|
|
421
587
|
hIdx++;
|
|
422
588
|
}
|
|
423
589
|
}
|
|
424
|
-
|
|
590
|
+
// keep a turn when it has headings OR a time — turns without headings
|
|
591
|
+
// still get a standalone time entry in the outline (click to jump)
|
|
592
|
+
return result.filter(function (g) { return g.headings.length > 0 || g.time !== ""; });
|
|
425
593
|
}, [order, nodes]);
|
|
426
594
|
var groupTrees = react.useMemo(function () {
|
|
427
595
|
return groups.map(function (g) { return buildTree(g.headings); });
|
|
@@ -463,8 +631,10 @@ window.__ModuleLoader__.load({
|
|
|
463
631
|
if (sig !== activeSigRef.current) {
|
|
464
632
|
activeSigRef.current = sig;
|
|
465
633
|
setActiveGroup(actives);
|
|
466
|
-
// auto-follow: keep the reading position visible in the outline
|
|
467
|
-
|
|
634
|
+
// auto-follow: keep the reading position visible in the outline —
|
|
635
|
+
// but pause for ~2s after the user touches the outline themselves,
|
|
636
|
+
// otherwise loading older turns gets yanked back to the bottom
|
|
637
|
+
if (actives.length > 0 && Date.now() - outlineTouchRef.current > 2000) {
|
|
468
638
|
var gi0 = actives[0];
|
|
469
639
|
// ensure the group's window is loaded (with a small buffer below)
|
|
470
640
|
setVisibleCount(function (prev) {
|
|
@@ -491,6 +661,23 @@ window.__ModuleLoader__.load({
|
|
|
491
661
|
return function () { sp.removeEventListener("scroll", update); };
|
|
492
662
|
}, [keyToGroup]);
|
|
493
663
|
|
|
664
|
+
// ---- search matches: headings whose title contains the query ----
|
|
665
|
+
var matches = react.useMemo(function () {
|
|
666
|
+
var q = query.trim().toLowerCase();
|
|
667
|
+
if (!q) return [];
|
|
668
|
+
var out = [];
|
|
669
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
670
|
+
var g = groups[gi];
|
|
671
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
672
|
+
var h = g.headings[j];
|
|
673
|
+
if (h.title.toLowerCase().indexOf(q) >= 0) {
|
|
674
|
+
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
679
|
+
}, [groups, query]);
|
|
680
|
+
|
|
494
681
|
// ---- nothing to show without headings ----
|
|
495
682
|
if (groups.length === 0) return null;
|
|
496
683
|
|
|
@@ -502,7 +689,19 @@ window.__ModuleLoader__.load({
|
|
|
502
689
|
? window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW
|
|
503
690
|
: (viewport ? viewport.left + 8 : 8);
|
|
504
691
|
} else {
|
|
505
|
-
|
|
692
|
+
// collapsed position:
|
|
693
|
+
// left dock -> slide just past the left sidebar edge (stop there)
|
|
694
|
+
// right dock -> boundary is the right sidebar when it is open,
|
|
695
|
+
// otherwise the screen's right edge
|
|
696
|
+
if (dockRight) {
|
|
697
|
+
// with the right sidebar open, slide INTO the sidebar area (covered
|
|
698
|
+
// by the cover wall); otherwise fly off the screen's right edge
|
|
699
|
+
panelLeft = (viewport && viewport.right > 80)
|
|
700
|
+
? (window.innerWidth - viewport.right)
|
|
701
|
+
: window.innerWidth + 24;
|
|
702
|
+
} else {
|
|
703
|
+
panelLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
|
|
704
|
+
}
|
|
506
705
|
}
|
|
507
706
|
var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
|
|
508
707
|
var baseTopPx = (viewport ? viewport.top + 14 : window.innerHeight * 0.12) + panelY;
|
|
@@ -555,6 +754,24 @@ window.__ModuleLoader__.load({
|
|
|
555
754
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
556
755
|
};
|
|
557
756
|
|
|
757
|
+
// jump to the n-th match, cycling; also reveal the group in the outline
|
|
758
|
+
var goToMatch = function (n) {
|
|
759
|
+
if (matches.length === 0) return;
|
|
760
|
+
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
761
|
+
setMatchIdx(i);
|
|
762
|
+
var m = matches[i];
|
|
763
|
+
jump(m.key, m.idx);
|
|
764
|
+
setVisibleCount(function (prev) {
|
|
765
|
+
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
766
|
+
});
|
|
767
|
+
setTimeout(function () {
|
|
768
|
+
var el = listRef.current;
|
|
769
|
+
if (!el) return;
|
|
770
|
+
var node = el.querySelector('[data-group-idx="' + m.gi + '"]');
|
|
771
|
+
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
772
|
+
}, 150);
|
|
773
|
+
};
|
|
774
|
+
|
|
558
775
|
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
559
776
|
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
560
777
|
var ox = offset ? (offset.x || 0) : 0;
|
|
@@ -599,8 +816,26 @@ window.__ModuleLoader__.load({
|
|
|
599
816
|
};
|
|
600
817
|
|
|
601
818
|
// ---- panel ----
|
|
602
|
-
// panel:
|
|
603
|
-
|
|
819
|
+
// panel: opening slides in with a slow fade; closing slides quickly to
|
|
820
|
+
// the dock edge, clipped by the sidebar line (looks covered, not
|
|
821
|
+
// dissolving) and only fades at the very end. No box-shadow: a shadow
|
|
822
|
+
// would get cut in half by the clip-path, so the panel is flat.
|
|
823
|
+
var panelTransition = open
|
|
824
|
+
? "left 0.6s " + EASE + ", clip-path 0.6s " + EASE + ", opacity 0.45s ease"
|
|
825
|
+
: "left 0.28s " + EASE + ", clip-path 0.28s " + EASE + ", opacity 0.14s ease 0.26s";
|
|
826
|
+
|
|
827
|
+
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
828
|
+
// like being covered by the sidebar (the sidebar stays untouched):
|
|
829
|
+
// left dock -> clipped from the left up to the sidebar line
|
|
830
|
+
// right dock -> clipped from the right at the screen/sidebar line
|
|
831
|
+
var panelClip = "inset(0 0 0 0px)";
|
|
832
|
+
if (!open) {
|
|
833
|
+
if (dockRight) {
|
|
834
|
+
panelClip = "inset(0 " + panelW + "px 0 0)";
|
|
835
|
+
} else {
|
|
836
|
+
panelClip = "inset(0 0 0 " + (panelW + 8) + "px)";
|
|
837
|
+
}
|
|
838
|
+
}
|
|
604
839
|
var panelOpacity = open ? (hovered ? 0.95 : 0.45) : 0;
|
|
605
840
|
|
|
606
841
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
@@ -621,6 +856,8 @@ window.__ModuleLoader__.load({
|
|
|
621
856
|
overflow: "hidden",
|
|
622
857
|
color: C.text,
|
|
623
858
|
opacity: panelOpacity,
|
|
859
|
+
clipPath: panelClip,
|
|
860
|
+
boxShadow: "none",
|
|
624
861
|
transition: panelTransition,
|
|
625
862
|
pointerEvents: "auto"
|
|
626
863
|
},
|
|
@@ -693,15 +930,164 @@ window.__ModuleLoader__.load({
|
|
|
693
930
|
react_jsx_runtime.jsx("div", {
|
|
694
931
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
695
932
|
children: [
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
933
|
+
// magnifier button (SVG, matches the other buttons' style)
|
|
934
|
+
react_jsx_runtime.jsx("button", {
|
|
935
|
+
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
936
|
+
title: "搜索标题",
|
|
937
|
+
style: {
|
|
938
|
+
width: 24,
|
|
939
|
+
height: 24,
|
|
940
|
+
display: "flex",
|
|
941
|
+
alignItems: "center",
|
|
942
|
+
justifyContent: "center",
|
|
943
|
+
borderRadius: "50%",
|
|
944
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
945
|
+
border: "none",
|
|
946
|
+
color: C.muted,
|
|
947
|
+
cursor: "pointer"
|
|
948
|
+
},
|
|
949
|
+
onMouseEnter: function (e) {
|
|
950
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
951
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
952
|
+
},
|
|
953
|
+
onMouseLeave: function (e) {
|
|
954
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
955
|
+
e.currentTarget.style.color = C.muted;
|
|
956
|
+
},
|
|
957
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
958
|
+
width: 15,
|
|
959
|
+
height: 15,
|
|
960
|
+
viewBox: "0 0 24 24",
|
|
961
|
+
// overflow visible: the stroke may extend past the
|
|
962
|
+
// viewBox without being clipped at the svg boundary
|
|
963
|
+
style: { display: "block", transform: "translate(-1px, -1px)", overflow: "visible" },
|
|
964
|
+
fill: "none",
|
|
965
|
+
stroke: "currentColor",
|
|
966
|
+
strokeWidth: 3.6,
|
|
967
|
+
strokeLinecap: "round",
|
|
968
|
+
children: [
|
|
969
|
+
react_jsx_runtime.jsx("circle", { cx: 11, cy: 11, r: 7 }),
|
|
970
|
+
react_jsx_runtime.jsx("line", { x1: 21, y1: 21, x2: 16, y2: 16 })
|
|
971
|
+
]
|
|
972
|
+
})
|
|
973
|
+
}),
|
|
974
|
+
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
975
|
+
iconBtn(toggleDock, dockRight ? "移到左侧" : "移到右侧", dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
976
|
+
// close: thick SVG cross, nudged slightly down
|
|
977
|
+
react_jsx_runtime.jsx("button", {
|
|
978
|
+
onClick: function () { setOpen(false); },
|
|
979
|
+
title: "收起",
|
|
980
|
+
style: {
|
|
981
|
+
width: 24,
|
|
982
|
+
height: 24,
|
|
983
|
+
display: "flex",
|
|
984
|
+
alignItems: "center",
|
|
985
|
+
justifyContent: "center",
|
|
986
|
+
borderRadius: "50%",
|
|
987
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
988
|
+
border: "none",
|
|
989
|
+
color: C.muted,
|
|
990
|
+
cursor: "pointer"
|
|
991
|
+
},
|
|
992
|
+
onMouseEnter: function (e) {
|
|
993
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
994
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
995
|
+
},
|
|
996
|
+
onMouseLeave: function (e) {
|
|
997
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
998
|
+
e.currentTarget.style.color = C.muted;
|
|
999
|
+
},
|
|
1000
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1001
|
+
width: 15,
|
|
1002
|
+
height: 15,
|
|
1003
|
+
viewBox: "0 0 24 24",
|
|
1004
|
+
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
1005
|
+
fill: "none",
|
|
1006
|
+
stroke: "currentColor",
|
|
1007
|
+
strokeWidth: 3.4,
|
|
1008
|
+
strokeLinecap: "round",
|
|
1009
|
+
children: [
|
|
1010
|
+
// cross lines span 5..19, intersection (12,12) centered
|
|
1011
|
+
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
1012
|
+
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
1013
|
+
]
|
|
1014
|
+
})
|
|
1015
|
+
})
|
|
699
1016
|
]
|
|
700
1017
|
})
|
|
701
1018
|
]
|
|
702
1019
|
})
|
|
703
1020
|
]
|
|
704
1021
|
}),
|
|
1022
|
+
// search input row: outer grid row animates 0fr<->1fr so the row
|
|
1023
|
+
// collapses/expands to its EXACT natural height (no max-height
|
|
1024
|
+
// overshoot stutter); the outline below moves smoothly with it
|
|
1025
|
+
(searchOpen || searchAnim === "out") ? react_jsx_runtime.jsx("div", {
|
|
1026
|
+
style: {
|
|
1027
|
+
display: "grid",
|
|
1028
|
+
gridTemplateRows: (searchAnim === "enter" || searchAnim === "out") ? "0fr" : "1fr",
|
|
1029
|
+
transition: "grid-template-rows 0.2s " + EASE,
|
|
1030
|
+
flex: "none"
|
|
1031
|
+
},
|
|
1032
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1033
|
+
style: { overflow: "hidden", minHeight: 0 },
|
|
1034
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1035
|
+
style: {
|
|
1036
|
+
display: "flex",
|
|
1037
|
+
alignItems: "center",
|
|
1038
|
+
gap: 4,
|
|
1039
|
+
padding: "2px 8px 4px",
|
|
1040
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
1041
|
+
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
1042
|
+
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
1043
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
1044
|
+
},
|
|
1045
|
+
children: [
|
|
1046
|
+
react_jsx_runtime.jsx("input", {
|
|
1047
|
+
ref: searchRef,
|
|
1048
|
+
value: query,
|
|
1049
|
+
onChange: function (e) { onQueryChange(e.target.value); },
|
|
1050
|
+
onKeyDown: function (e) {
|
|
1051
|
+
if (e.key === "Enter") { goToMatch(matchIdx + 1); e.preventDefault(); }
|
|
1052
|
+
if (e.key === "Escape") { closeSearch(); }
|
|
1053
|
+
},
|
|
1054
|
+
placeholder: "搜索标题,回车定位…",
|
|
1055
|
+
style: {
|
|
1056
|
+
flex: 1,
|
|
1057
|
+
minWidth: 0,
|
|
1058
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1059
|
+
border: "none",
|
|
1060
|
+
borderRadius: 6,
|
|
1061
|
+
padding: "3px 8px",
|
|
1062
|
+
fontSize: 12,
|
|
1063
|
+
color: C.text,
|
|
1064
|
+
outline: "none"
|
|
1065
|
+
}
|
|
1066
|
+
}),
|
|
1067
|
+
searching ? react_jsx_runtime.jsx("span", {
|
|
1068
|
+
style: { width: 13, height: 13, flex: "none", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
1069
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1070
|
+
width: 12,
|
|
1071
|
+
height: 12,
|
|
1072
|
+
viewBox: "0 0 24 24",
|
|
1073
|
+
style: { display: "block", animation: "dqt-spin 0.8s linear infinite" },
|
|
1074
|
+
children: react_jsx_runtime.jsx("circle", {
|
|
1075
|
+
cx: 12, cy: 12, r: 9,
|
|
1076
|
+
fill: "none",
|
|
1077
|
+
stroke: C.muted,
|
|
1078
|
+
strokeWidth: 3,
|
|
1079
|
+
strokeDasharray: "42 22",
|
|
1080
|
+
strokeLinecap: "round"
|
|
1081
|
+
})
|
|
1082
|
+
})
|
|
1083
|
+
}) : react_jsx_runtime.jsx("span", {
|
|
1084
|
+
style: { fontSize: 11, color: C.muted, flex: "none", minWidth: 30, textAlign: "center" },
|
|
1085
|
+
children: matches.length > 0 ? ((matchIdx % matches.length) + 1) + "/" + matches.length : ""
|
|
1086
|
+
})
|
|
1087
|
+
]
|
|
1088
|
+
})
|
|
1089
|
+
})
|
|
1090
|
+
}) : null,
|
|
705
1091
|
// outline list (paged: newest first, scroll to the top loads older);
|
|
706
1092
|
// the scrollbar follows the dock side (rtl flips it to the left)
|
|
707
1093
|
react_jsx_runtime.jsx("div", {
|
|
@@ -709,12 +1095,12 @@ window.__ModuleLoader__.load({
|
|
|
709
1095
|
style: {
|
|
710
1096
|
overflowY: "auto",
|
|
711
1097
|
padding: "6px 8px",
|
|
712
|
-
maxHeight: "60vh",
|
|
713
1098
|
flex: "1 1 auto",
|
|
714
1099
|
minHeight: 0,
|
|
715
1100
|
direction: dockRight ? "ltr" : "rtl"
|
|
716
1101
|
},
|
|
717
1102
|
onScroll: onListScroll,
|
|
1103
|
+
onWheel: onListWheel,
|
|
718
1104
|
children: react_jsx_runtime.jsx("div", {
|
|
719
1105
|
style: { direction: "ltr" },
|
|
720
1106
|
children: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup)
|
|
@@ -867,6 +1253,50 @@ window.__ModuleLoader__.load({
|
|
|
867
1253
|
return out;
|
|
868
1254
|
}
|
|
869
1255
|
|
|
1256
|
+
// a group's header: clicking the time jumps to that turn's start.
|
|
1257
|
+
// Implemented as a function (not an inline closure in a loop) so each
|
|
1258
|
+
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
1259
|
+
// otherwise make every header jump to the last group.
|
|
1260
|
+
function renderGroupHeader(g, gi, jump, C) {
|
|
1261
|
+
var jumpToTurn = function (e) {
|
|
1262
|
+
e.stopPropagation();
|
|
1263
|
+
if (!g.userKey) return;
|
|
1264
|
+
var r0 = findRowStrict(g.userKey);
|
|
1265
|
+
if (r0) jump(g.userKey);
|
|
1266
|
+
else if (g.headings.length > 0) jump(g.headings[0].key, g.headings[0].idx);
|
|
1267
|
+
};
|
|
1268
|
+
return react_jsx_runtime.jsx("div", {
|
|
1269
|
+
style: { padding: "1px 4px 2px", height: 18, display: "flex", alignItems: "center", minWidth: 0 },
|
|
1270
|
+
children: react_jsx_runtime.jsx("span", {
|
|
1271
|
+
onClick: jumpToTurn,
|
|
1272
|
+
title: g.userKey ? "跳转到该回合开头" : "",
|
|
1273
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
1274
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
1275
|
+
style: {
|
|
1276
|
+
fontSize: 11,
|
|
1277
|
+
color: C.muted,
|
|
1278
|
+
cursor: "pointer",
|
|
1279
|
+
padding: "1px 5px",
|
|
1280
|
+
borderRadius: 4,
|
|
1281
|
+
transition: "background 0.15s ease",
|
|
1282
|
+
display: "inline-flex",
|
|
1283
|
+
alignItems: "center",
|
|
1284
|
+
gap: 5,
|
|
1285
|
+
minWidth: 0,
|
|
1286
|
+
maxWidth: "100%",
|
|
1287
|
+
overflow: "hidden"
|
|
1288
|
+
},
|
|
1289
|
+
children: [
|
|
1290
|
+
react_jsx_runtime.jsx("span", { style: { fontWeight: 600, flex: "none" }, children: g.time || " " }),
|
|
1291
|
+
g.userText ? react_jsx_runtime.jsx("span", {
|
|
1292
|
+
style: { fontWeight: 400, opacity: 0.75, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
|
|
1293
|
+
children: g.userText
|
|
1294
|
+
}) : null
|
|
1295
|
+
]
|
|
1296
|
+
})
|
|
1297
|
+
}, "g-h-" + gi);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
870
1300
|
// render each conversation turn as its own block: solid divider + time header
|
|
871
1301
|
// offset = global group index of the first rendered group (stable React keys)
|
|
872
1302
|
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
@@ -884,19 +1314,12 @@ window.__ModuleLoader__.load({
|
|
|
884
1314
|
items.push(react_jsx_runtime.jsx("div", {
|
|
885
1315
|
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
886
1316
|
}, "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
|
-
}
|
|
1317
|
+
items.push(renderGroupHeader(g, gi, jump, C));
|
|
895
1318
|
items.push(renderNodes(trees[i], 0, jump, C, "g" + gi));
|
|
896
1319
|
var dim = !activeSet[gi];
|
|
897
1320
|
out.push(react_jsx_runtime.jsx("div", {
|
|
898
1321
|
"data-group-idx": gi,
|
|
899
|
-
style: { opacity: dim ? 0.
|
|
1322
|
+
style: { opacity: dim ? 0.6 : 1, transition: "opacity 0.3s ease" },
|
|
900
1323
|
children: items
|
|
901
1324
|
}, "g-" + gi));
|
|
902
1325
|
}
|
|
@@ -943,6 +1366,17 @@ window.__ModuleLoader__.load({
|
|
|
943
1366
|
|
|
944
1367
|
exports.apply = apply;
|
|
945
1368
|
exports.inject = inject;
|
|
1369
|
+
|
|
1370
|
+
// inject the small keyframe stylesheet once (spinner rotation)
|
|
1371
|
+
(function () {
|
|
1372
|
+
if (typeof document === "undefined") return;
|
|
1373
|
+
if (document.getElementById("dsh-quick-toc-css")) return;
|
|
1374
|
+
var s = document.createElement("style");
|
|
1375
|
+
s.id = "dsh-quick-toc-css";
|
|
1376
|
+
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}";
|
|
1377
|
+
document.head.appendChild(s);
|
|
1378
|
+
})();
|
|
1379
|
+
|
|
946
1380
|
return module.exports;
|
|
947
1381
|
}
|
|
948
1382
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta",
|
|
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
|
}
|