dsh-quick-toc 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.7.4] - 2026-09-23
6
+
7
+ ### Fixed
8
+ - **Opening the curtain had no animation on a long session**: opening the curtain made the outline's own auto-loader pull the whole session in at once (measured: DOM nodes 1365 to 46150, single long tasks of 275-897ms), so the curtain's 0.46s drop was over before a frame of it could paint. Automatic loading now requires a real reader gesture (a wheel tick on the list, a key on the panel, a press on the list); a geometric coincidence (a short list that happens to fit, a freshly opened curtain, a resized window) is no longer treated as a load request.
9
+ - **Switching the dock made the panel disappear on one side and slide in from the other**: the switch moved `left`, which is not in the transition list, so the box teleported while only the offset was tweened. The panel now always sits on one fixed anchor, and its open, collapsed, either-dock and curtain-handoff positions are all offsets on that anchor, so a dock switch is a pure translate animation.
10
+ - **The collapsed panel came to rest left of the position it opens from (8px on the left dock)**: the collapse clip was a fixed panel width (plus 8px on the left dock) while the box actually travelled a panel width plus 16px, so the visible edge drifted. The clip now matches the travel, which keeps the visible edge on the panel's own open edge line all the way out, the same on both docks.
11
+ - **A collapse occasionally came to rest in the wrong place**: the host re-measures the conversation area while the collapse is running, which moves the line the panel aims at (measured: the tuck-in landed at 1396 instead of 1526). The open-to-closed step now freezes the geometry that collapse started with; the next open reads the area afresh.
12
+
5
13
  ## [0.7.3] - 2026-09-23
6
14
 
7
15
  ### Changed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  **dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.7.4] - 2026-09-23
6
+
7
+ ### 修复
8
+ - **长会话里打开幕布没有展开动画**:打开幕布时,本插件自己的自动加载把整段会话一次性拉了进来(实测 DOM 节点 1365 → 46150、单帧长任务 275–897ms),0.46 秒的展开动画在第一帧画出来之前就播完了。自动加载现在必须由读者的真实操作触发(列表上的滚轮、面板上的按键、在列表上按下),几何上的巧合(短列表恰好装得下、刚打开幕布、窗口改变尺寸)不再被当成加载请求。
9
+ - **切换停靠边时面板先消失、再从另一边滑出来**:切换原本移动 `left`,而 `left` 不在过渡列表里——盒子瞬移,只有位移在补间。面板现在始终停在一个固定锚点上,展开、收起、两个停靠边、交给幕布这四个位置都是锚点上的位移,切换停靠边是一段纯位移动画。
10
+ - **收起后停的位置比展开位置偏左(左停靠实测 8px)**:收起的裁剪量原本是面板宽度(左停靠另加 8px),与盒子真正滑动的距离(左停靠为面板宽度 + 16px)不相等,可见边一路漂移。裁剪量改为与实际滑动距离一致后,可见边全程钉在面板自己那条开合线上,左右停靠对称。
11
+ - **收起时偶尔收偏**:宿主会在收起动画途中重新测量会话区,目标线随之改变(实测收进去的位置从 1526 跑到 1396)。收起开始的那一步现在冻结本次动画使用的几何,之后再展开才读取新几何。
12
+
5
13
  ## [0.7.3] - 2026-09-23
6
14
 
7
15
  ### 变更
package/README.en.md CHANGED
@@ -42,7 +42,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
42
42
 
43
43
  | Plugin | Supported DSH |
44
44
  | --- | --- |
45
- | **0.7.x** (latest, 0.7.3) | 0.1.5-rc.1, 0.1.5-rc.2, 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2 |
45
+ | **0.7.x** (latest, 0.7.4) | 0.1.5-rc.1, 0.1.5-rc.2, 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2 |
46
46
  | 0.6.x (0.6.3) | 0.1.5-rc.1, 0.1.5-rc.2 |
47
47
  | 0.5.x (0.5.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
48
48
  | 0.4.x (0.4.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
package/README.md CHANGED
@@ -42,7 +42,7 @@
42
42
 
43
43
  | 插件版本 | 支持的 DSH 版本 |
44
44
  | --- | --- |
45
- | **0.7.x**(最新,0.7.3) | 0.1.5-rc.1、0.1.5-rc.2、0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2 |
45
+ | **0.7.x**(最新,0.7.4) | 0.1.5-rc.1、0.1.5-rc.2、0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2 |
46
46
  | 0.6.x(0.6.3) | 0.1.5-rc.1、0.1.5-rc.2 |
47
47
  | 0.5.x(0.5.1) | 0.1.5-rc.1、0.1.5-rc.2 |
48
48
  | 0.4.x(0.4.1) | 0.1.5-rc.1、0.1.5-rc.2 |
package/lib/client.js CHANGED
@@ -51,6 +51,10 @@ window.__ModuleLoader__.load({
51
51
  // panel's own collapse duration (0.28s) plus a hair, so the curtain's drop only
52
52
  // starts once the panel has actually finished travelling.
53
53
  var PANEL_TUCK_MS = 300;
54
+ // How long a collapse keeps the edge it started from (see the geometry block): the
55
+ // panel's collapse takes 0.28s, and the host can re-measure the conversation area at
56
+ // any point while it runs, so the freeze outlives the motion by a frame or two.
57
+ var COLLAPSE_HOLD_MS = 420;
54
58
  // The row-end control ("jump to the end of this section") lives in the last 24px
55
59
  // of a row (a 20px button inset 4px). Every box a row can paint — the hover
56
60
  // highlight, a group header's hover label, the right-aligned context text — stops
@@ -1719,8 +1723,16 @@ window.__ModuleLoader__.load({
1719
1723
  var autoWatchRef = react.useRef(null); // did the pull we started actually land? (fallback click)
1720
1724
  var autoProbeRef = react.useRef(0); // throttle for the on-scroll approach probe
1721
1725
  var autoStampRef = react.useRef(0);
1726
+ var autoGestureRef = react.useRef(0); // last REAL reader gesture on the outline list
1722
1727
  var autoLoadRef = react.useRef(true);
1723
1728
  var panelVisibleRef = react.useRef(true);
1729
+ // A collapse keeps the geometry it started with: the host re-measures the
1730
+ // conversation area while our panel is sliding away, and reading the new numbers
1731
+ // live moves the line the panel tucks into — it then settles somewhere other than
1732
+ // the line it slid out from. While a collapse runs, the edge it aims at is frozen.
1733
+ var geoOpenRef = react.useRef(null);
1734
+ var geoHoldRef = react.useRef(0);
1735
+ var geoSnapRef = react.useRef(null);
1724
1736
  // The reader's place in the outline: sampled by the scroll handler and right
1725
1737
  // before any change of our own, restored in a LAYOUT effect (see the paging
1726
1738
  // block). `anchorHintRef` remembers where the visible boundary was, so finding it
@@ -2586,6 +2598,15 @@ window.__ModuleLoader__.load({
2586
2598
  var AUTO_WAIT_MS = 40; // beat while a page is in flight: the host is the limit
2587
2599
  var AUTO_BURST_MAX = 60; // runaway guard only: a long gap must stay walkable
2588
2600
  var AUTO_BURST_IDLE_MS = 3000; // quiet this long and the budget is fresh again
2601
+ // Loading is something the READER asks for, so every automatic pull must be
2602
+ // answered by a real gesture (a wheel tick, a press, a key) within this window.
2603
+ // Purely geometric triggers — a freshly laid out list whose few rows all fit the
2604
+ // box, a panel/curtain opened, a window resized — look exactly like "an unloaded
2605
+ // turn is in view" without anyone having asked: opening the curtain once made this
2606
+ // loader call `loadThrough` on the first ghost it found, which materialised the
2607
+ // rest of the session (46k nodes, 300–900ms long tasks) before the curtain's own
2608
+ // 0.46s drop animation could paint. A gesture-gated loader cannot do that.
2609
+ var AUTO_GESTURE_MS = 1500;
2589
2610
  var ghostInView = function (el) {
2590
2611
  var ghosts = el.querySelectorAll("[data-ghost-turn]");
2591
2612
  if (ghosts.length === 0) return null;
@@ -2728,6 +2749,10 @@ window.__ModuleLoader__.load({
2728
2749
  return true;
2729
2750
  }
2730
2751
  if (historyExhausted(face)) return false;
2752
+ // Nothing the reader did — no wheel, no press, no key — since the window above:
2753
+ // a geometric coincidence (a fresh list that happens to fit, a newly opened
2754
+ // curtain, a resized window) is not a request to load history.
2755
+ if (Date.now() - autoGestureRef.current > AUTO_GESTURE_MS) return false;
2731
2756
  // A ghost the reader can actually see, or the gap coming up on them.
2732
2757
  if (ghostTurn !== null || frontierApproaching(el)) {
2733
2758
  // ARRIVED at an unloaded turn: cover it in one host call (`loadThrough`), which is
@@ -2815,6 +2840,7 @@ window.__ModuleLoader__.load({
2815
2840
  // always refreshes older turns, even without a visible scrollbar
2816
2841
  var onListWheel = function (e) {
2817
2842
  outlineTouchRef.current = Date.now();
2843
+ autoGestureRef.current = Date.now(); // a wheel tick is a reader gesture
2818
2844
  var el = listRef.current;
2819
2845
  if (!el) return;
2820
2846
  hoverEnd();
@@ -3964,9 +3990,23 @@ window.__ModuleLoader__.load({
3964
3990
  // ---- nothing to show without headings ----
3965
3991
  if (groups.length === 0) return null;
3966
3992
 
3967
- // ---- geometry: always position by `left` (px) so dock-switch & slide share one animated transition
3968
- // (The scale preference leaves all of this alone: it acts on the panel's inner
3969
- // box, so the panel itself keeps the width and height the reader dragged.)
3993
+ // ---- geometry: ONE parked anchor; the dock side lives in the transform --------------
3994
+ // The box is parked at a single spot — off-area to the LEFT of the conversation — and
3995
+ // every position the panel takes (open, collapsed on either dock, handed to the
3996
+ // curtain) is a `transform` offset from there. Two reasons:
3997
+ // * a dock switch then animates the TRANSFORM alone, so the panel travels across the
3998
+ // screen on the compositor. Docking used to move `left` (which is not in the
3999
+ // transition list): the box teleported to the other side while only the transform
4000
+ // animated, so the panel vanished on one side and slid in from the other.
4001
+ // * `left` must never be animated: it re-lays out the whole panel — header, toolbar
4002
+ // and every rendered row — on every frame, and with a long session loaded that
4003
+ // layout cannot keep up (the wipe ran while the slide stalled, and the panel
4004
+ // flickered). Adding `left` to the transition for a dock switch was measured as
4005
+ // "very laggy and flashing" for exactly this reason.
4006
+ // The COLLAPSED spot is per dock (left: the anchor itself; right: one panel width past
4007
+ // the open position), which is what makes the collapse fly out toward its own edge.
4008
+ // (The scale preference leaves all of this alone: it acts on the panel's inner box,
4009
+ // so the panel itself keeps the width and height the reader dragged.)
3970
4010
  var dockRight = dock === "right";
3971
4011
  // Keep the OPEN panel inside the window whatever the conversation area does.
3972
4012
  // The area is the host's to move: its transcript width grips rewrite the
@@ -3980,27 +4020,48 @@ window.__ModuleLoader__.load({
3980
4020
  return Math.max(8, Math.min(Math.max(8, window.innerWidth - panelW - 8), x));
3981
4021
  };
3982
4022
  // While the panel is being handed over to (or back from) the curtain it is
3983
- // rendered at its COLLAPSED geometry, so the same left/clip transition that
4023
+ // rendered at its COLLAPSED geometry, so the same transform/clip transition that
3984
4024
  // serves open/collapse also serves the handoff: no teleport, no flash.
3985
4025
  var geoOpen = open && !panelAside;
3986
- // The box is always PARKED at its collapsed spot and the open state is a transform
3987
- // offset on top of it (see panelTransition): same visual geometry as before, but the
3988
- // slide is compositable. `openLeft` is the clamped open position, `parkedLeft` the
3989
- // off-area spot the collapsed panel occupies.
3990
- var openLeft, parkedLeft;
4026
+ var anchorLeft = viewport ? viewport.left - panelW - 8 : -(panelW + 48);
4027
+ var openLeft;
3991
4028
  if (dockRight) {
3992
4029
  // RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
3993
4030
  // line when collapsed as when expanded (the expand edge is correct).
3994
4031
  var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
3995
4032
  openLeft = clampLeft(openRight);
3996
- parkedLeft = openRight + panelW;
3997
4033
  } else {
3998
4034
  // LEFT dock open (left dock is the confirmed-correct reference — untouched)
3999
4035
  openLeft = clampLeft(viewport ? viewport.left + 8 : 8);
4000
- parkedLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
4001
4036
  }
4002
- var panelLeft = parkedLeft;
4003
- var panelOffset = geoOpen ? (openLeft - parkedLeft) : 0;
4037
+ // A collapse finishes at the line it started from. The area we park against can be
4038
+ // re-measured while the panel is still sliding away — the host rewrites it when the
4039
+ // transcript lays out, and the window itself can change — and aiming at the new
4040
+ // numbers mid-flight lands the panel somewhere other than the edge it came out of
4041
+ // (measured: with the area re-measured during the collapse, the last visible strip
4042
+ // sat at 1396 instead of the 1526 line the panel had slid out from). So the
4043
+ // open -> closed step freezes the edge the collapse heads for, and the next open —
4044
+ // which reads the area afresh — picks up the new geometry. A dock switch cancels the
4045
+ // freeze: that is the reader asking for the other side, not a re-measure. The first
4046
+ // render arms nothing: a panel that was never open has no edge to hold, and pinning
4047
+ // the pre-measurement fallback would leave the closed box where no such panel goes.
4048
+ if (geoOpenRef.current === null) {
4049
+ geoOpenRef.current = geoOpen;
4050
+ } else if (geoOpenRef.current !== geoOpen) {
4051
+ geoOpenRef.current = geoOpen;
4052
+ geoHoldRef.current = geoOpen ? 0 : Date.now() + COLLAPSE_HOLD_MS;
4053
+ geoSnapRef.current = geoOpen ? null : { anchorLeft: anchorLeft, openLeft: openLeft, dock: dock };
4054
+ }
4055
+ var geoFrozen = geoSnapRef.current !== null && geoSnapRef.current.dock === dock
4056
+ && Date.now() < geoHoldRef.current;
4057
+ if (geoFrozen) {
4058
+ anchorLeft = geoSnapRef.current.anchorLeft;
4059
+ openLeft = geoSnapRef.current.openLeft;
4060
+ }
4061
+ var openOffset = openLeft - anchorLeft;
4062
+ var parkedOffset = dockRight ? (openLeft + panelW) - anchorLeft : 0;
4063
+ var panelLeft = anchorLeft;
4064
+ var panelOffset = geoOpen ? openOffset : parkedOffset;
4004
4065
  var panelTransform = panelOffset === 0 ? "none" : "translateX(" + zLen(panelOffset) + ")";
4005
4066
  var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
4006
4067
  // same reasoning vertically: the draggable offset is stored in localStorage
@@ -4653,6 +4714,8 @@ window.__ModuleLoader__.load({
4653
4714
  // the search box owns its own keys (it stops propagation for the ones it uses)
4654
4715
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (target && target.isContentEditable)) return;
4655
4716
  if (e.altKey || e.ctrlKey || e.metaKey) return;
4717
+ // a key on the panel is a reader gesture: the auto-loader may act on it
4718
+ autoGestureRef.current = Date.now();
4656
4719
  var key = e.key;
4657
4720
  if (key === "ArrowDown" || key === "ArrowUp") {
4658
4721
  swallow(e);
@@ -4752,13 +4815,22 @@ window.__ModuleLoader__.load({
4752
4815
  // right dock -> clipped from the right at the screen/sidebar line
4753
4816
  // LEFT dock keeps its original clip (collapse left). RIGHT dock clips the
4754
4817
  // LEFT side so the panel closes toward its right (dock) edge in place.
4818
+ //
4819
+ // The inset is the distance the box actually TRAVELS, not the panel's width. The
4820
+ // visible edge of the panel is the clip edge, and that edge is `box.left + inset`
4821
+ // (left dock) / `box.right - inset` (right dock) while the box slides; matching the
4822
+ // inset to the travel makes the two move at the same rate, so the edge stays pinned
4823
+ // on the panel's OWN open edge line the whole way out. With a fixed `panelW + 8`
4824
+ // against a `panelW + 16` travel the edge drifted 8px past that line, which the
4825
+ // reader saw as the panel tucking in slightly left of where it comes back out.
4826
+ var travelPx = Math.abs(openOffset - parkedOffset);
4755
4827
  var panelClip = "inset(0 0 0 0px)";
4756
4828
  if (!geoOpen) {
4757
4829
  if (dockRight) {
4758
4830
  // mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
4759
- panelClip = "inset(0 " + zLen(panelW) + " 0 0)";
4831
+ panelClip = "inset(0 " + zLen(travelPx) + " 0 0)";
4760
4832
  } else {
4761
- panelClip = "inset(0 0 0 " + zLen(panelW + 8) + ")";
4833
+ panelClip = "inset(0 0 0 " + zLen(travelPx) + ")";
4762
4834
  }
4763
4835
  }
4764
4836
  // faded out entirely while another center-column view is active
@@ -5434,6 +5506,9 @@ window.__ModuleLoader__.load({
5434
5506
  },
5435
5507
  onScroll: onListScroll,
5436
5508
  onWheel: onListWheel,
5509
+ // a press on a row (or on the list) is a reader gesture too: click-to-load an
5510
+ // unloaded row and drag-scrolling must both keep working
5511
+ onPointerDown: function () { autoGestureRef.current = Date.now(); },
5437
5512
  children: react_jsx_runtime.jsx("div", {
5438
5513
  // the two-phase cross-fade of the questions-only switch rides on this
5439
5514
  // wrapper (classes, never inline animation: React rewrites inline styles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, a level filter, a full-width curtain, jump-to-end buttons, title/full-text/cross-session search and a Chinese/English interface.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",