dsh-quick-toc 0.10.3 → 0.10.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,23 @@
2
2
 
3
3
  All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.10.4] - 2026-11-04
6
+
7
+ ### Added
8
+ - **Choice of views**: a new "Views" setting switches between panel + curtain, panel only and curtain only; with "curtain only" the docked panel is no longer built.
9
+ - **Position indicator**: a column of ticks and a slider on the right of the outline shows how far into the conversation you are; the style is either a bar or a soft band, and clicking a tick jumps to that section.
10
+
11
+ ### Changed
12
+ - **The declared host range now starts at 0.2.1-rc.2**: earlier prereleases are no longer listed as supported.
13
+ - **In the curtain the outline and the position indicator travel as one column**: the column is centred and moves with the search and bookmark sidebars, so the indicator stays beside the outline.
14
+ - **The "auto-load history" setting is gone**: history loads one page each time you reach the top, with no switch.
15
+ - **Reaching the top loads one page per arrival**: the wheel and the slider follow the same rule, matching the host's own "load earlier".
16
+
17
+ ### Fixed
18
+ - **Translucency stayed off for good on a slow device**: after it has been switched off automatically, turning the switch back on now takes effect.
19
+ - **Dragging the slider to the top left it stuck there**: it can be dragged back down again.
20
+ - **Unloaded rows appeared while scrolling up**: a page no longer leaves placeholder rows behind.
21
+
5
22
  ## [0.10.3] - 2026-10-28
6
23
 
7
24
  ### Changed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  **dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.10.4] - 2026-11-04
6
+
7
+ ### 新增
8
+ - **视图选择**:设置里新增「视图」,可选「面板 + 幕布」「仅面板」「仅幕布」;选「仅幕布」时不再构建停靠面板。
9
+ - **位置指示**:大纲右侧新增一列刻度与滑块,显示读到会话的哪个位置;样式可选「竖条」或「淡框」,点击刻度可跳到该节。
10
+
11
+ ### 变更
12
+ - **兼容范围收窄到 0.2.1-rc.2 及以上**:更早的预发布版本不再声明为兼容。
13
+ - **幕布里的大纲与位置指示成为一列**:整列居中,打开搜索或书签侧栏时一起移动,位置指示始终贴着大纲。
14
+ - **「自动加载历史」设置已移除**:历史改为每次到达顶部时加载一页,不再有开关。
15
+ - **到达顶部时一次只加载一页**:滚轮和拖动滑块同样按页加载,与宿主的「加载更早」一致。
16
+
17
+ ### 修复
18
+ - **设备较慢时半透明被永久关闭**:自动关闭后,手动重新打开开关不再被此前记下的结果挡住。
19
+ - **把滑块拖到顶部后无法再向下拖**:现在可以正常拖回。
20
+ - **向上滚动时出现未加载的条目**:加载一页之后不再出现只占位的条目。
21
+
5
22
  ## [0.10.3] - 2026-10-28
6
23
 
7
24
  ### 变更
package/README.en.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): it turns the Markdown headings (H1–H6) of AI replies into a navigable outline — the same list as a side panel or as a full-width curtain — grouped by conversation turn and covering the whole session (including turns that are not loaded yet), with title/prompts/full-text/cross-session search, hover previews, a right-click menu for jumping and copying, bookmarks with a bookmarks-only view, six theme colours and a translucent material, and a reading position that both follows and is remembered.
6
6
 
7
+ ## Screenshots
8
+
9
+ ![The side panel with a hover preview](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/preview.png)
10
+
11
+ | The curtain (full-width view) | Its bookmarks drawer |
12
+ | --- | --- |
13
+ | ![The curtain](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/curtain.png) | ![The bookmarks drawer](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/bookmarks.png) |
14
+
15
+ > Captured from an isolated instance against a demo conversation.
16
+
7
17
  ## Features
8
18
 
9
19
  - **Turn-grouped outline** — each user message plus its following AI replies form one group; the group header shows the turn's time and a first-line preview, and clicking it jumps to the start of that turn's model reply
@@ -44,7 +54,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
44
54
 
45
55
  | Plugin | Supported DSH |
46
56
  | --- | --- |
47
- | **0.10.x** (latest, 0.10.3) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2, 0.2.1-rc.3, 0.2.1-rc.4, 0.2.2-alpha.2 |
57
+ | **0.10.x** (latest, 0.10.4) | 0.2.1-rc.2, 0.2.1-rc.3, 0.2.1-rc.4, 0.2.1-rc.5, 0.2.2-alpha.2 |
48
58
  | **0.9.x** (0.9.5) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5 |
49
59
  | 0.8.x (0.8.2) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
50
60
  | 0.7.x (0.7.14) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
@@ -56,6 +66,8 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
56
66
 
57
67
  Each major line lists only its newest patch (the defects a new feature introduces are fixed in the patches that follow, so within one major line the newest patch is the one to use; older patches keep working — the plugin does not break existing interfaces).
58
68
 
69
+ From 0.10.4 the declared floor is 0.2.1-rc.2: earlier pre-releases are no longer supported — they can no longer open a session written by the current line, so the plugin's behaviour there cannot be verified.
70
+
59
71
  ## Install
60
72
 
61
73
  With the DSH CLI:
package/README.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  **DeepSeek Harness(DSH)对话大纲插件**:把 AI 回复中的 Markdown 标题(H1–H6)提取成可导航的大纲——侧边面板与全屏幕布两种视图,按对话回合分组、覆盖整段会话(含尚未加载进窗口的回合)。支持标题/提问/全文/跨会话四档检索、悬停预览、右键跳转与复制、书签与「只看书签」视图,六种主题色与半透明材质,阅读位置自动跟随并可记住。
6
6
 
7
+ ## 效果
8
+
9
+ ![侧边面板与悬停预览](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/preview.png)
10
+
11
+ | 幕布(全宽视图) | 幕布右侧的书签栏 |
12
+ | --- | --- |
13
+ | ![幕布](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/curtain.png) | ![书签栏](https://raw.githubusercontent.com/LyaxZ/dsh-quick-toc/main/assets/bookmarks.png) |
14
+
15
+ > 截图取自隔离环境里的实例,会话内容是用作演示的示例文案。
16
+
7
17
  ## 功能
8
18
 
9
19
  - **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示回合时间与首行预览,点击跳到该回合模型回答的开头
@@ -44,7 +54,7 @@
44
54
 
45
55
  | 插件版本 | 支持的 DSH 版本 |
46
56
  | --- | --- |
47
- | **0.10.x**(最新,0.10.3) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2、0.2.1-rc.3、0.2.1-rc.4、0.2.2-alpha.2 |
57
+ | **0.10.x**(最新,0.10.4) | 0.2.1-rc.2、0.2.1-rc.3、0.2.1-rc.4、0.2.1-rc.5、0.2.2-alpha.2 |
48
58
  | **0.9.x**(0.9.5) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5 |
49
59
  | 0.8.x(0.8.2) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
50
60
  | 0.7.x(0.7.14) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
@@ -56,6 +66,8 @@
56
66
 
57
67
  每个大版本只列该系列最新的一个补丁版本(新功能引入的缺陷都在其后的补丁里修掉了,所以同一个大版本内直接用最新补丁即可;旧补丁仍可继续用,插件不破坏既有接口)。
58
68
 
69
+ 0.10.4 起只声明 0.2.1-rc.2 及以上:更早的预发布版本不再支持,它们已无法打开当前版本写下的会话,插件在那里的行为无从验证。
70
+
59
71
  ## 安装
60
72
 
61
73
  通过 DSH CLI 安装:
package/icon.svg CHANGED
@@ -1,10 +1,10 @@
1
1
  <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <rect x="2.5" y="2.5" width="31" height="31" rx="9" fill="url(#tocbg)" />
3
- <rect x="6" y="14.5" width="24" height="7" rx="3.5" fill="#ffffff" fill-opacity="0.22" />
4
- <rect x="6" y="14.5" width="3" height="7" rx="1.5" fill="#1D4ED8" />
5
- <rect x="11" y="10.5" width="15" height="3.2" rx="1.6" fill="#FF7AB8" />
3
+ <rect x="6" y="14" width="24" height="8" rx="4" fill="#ffffff" fill-opacity="0.22" />
4
+ <rect x="6" y="14" width="3" height="8" rx="1.5" fill="#1D4ED8" />
5
+ <rect x="11" y="8.7" width="15" height="3.2" rx="1.6" fill="#FF7AB8" />
6
6
  <rect x="11" y="16.4" width="11.5" height="3.2" rx="1.6" fill="#FFB020" />
7
- <rect x="11" y="22.3" width="7.5" height="3.2" rx="1.6" fill="#2DD4BF" />
7
+ <rect x="11" y="24.1" width="7.5" height="3.2" rx="1.6" fill="#2DD4BF" />
8
8
  <defs>
9
9
  <linearGradient id="tocbg" x1="2.5" y1="2.5" x2="33.5" y2="33.5" gradientUnits="userSpaceOnUse">
10
10
  <stop stop-color="#6EA5FF" />
package/lib/client.js CHANGED
@@ -202,6 +202,13 @@ window.__ModuleLoader__.load({
202
202
  "settings.dock": "默认停靠边缘",
203
203
  "settings.dock.left": "左侧",
204
204
  "settings.dock.right": "右侧",
205
+ "settings.surface": "视图",
206
+ "settings.surface.both": "面板 + 幕布",
207
+ "settings.surface.panel": "仅面板",
208
+ "settings.surface.curtain": "仅幕布",
209
+ "settings.scrollbar": "位置指示",
210
+ "settings.scrollbar.bar": "竖条",
211
+ "settings.scrollbar.band": "淡框",
205
212
  "settings.levels": "显示的标题层级",
206
213
  "settings.density": "大纲密度",
207
214
  "settings.density.comfy": "标准",
@@ -230,7 +237,6 @@ window.__ModuleLoader__.load({
230
237
  "settings.fuzzy": "模糊搜索",
231
238
  "settings.hover": "悬停预览卡片",
232
239
  "settings.remember": "记住阅读位置",
233
- "settings.autoLoad": "自动加载历史",
234
240
  "settings.debug": "在控制台打印诊断日志",
235
241
  "settings.debug.tip": "开启后控制台会打印一行挂载诊断(宿主能力与跳转加载器状态)",
236
242
  "settings.overridden": "已自定义",
@@ -334,6 +340,13 @@ window.__ModuleLoader__.load({
334
340
  "settings.dock": "Default docked edge",
335
341
  "settings.dock.left": "Left",
336
342
  "settings.dock.right": "Right",
343
+ "settings.surface": "Views",
344
+ "settings.surface.both": "Panel + curtain",
345
+ "settings.surface.panel": "Panel only",
346
+ "settings.surface.curtain": "Curtain only",
347
+ "settings.scrollbar": "Position marker",
348
+ "settings.scrollbar.bar": "Bar",
349
+ "settings.scrollbar.band": "Band",
337
350
  "settings.levels": "Heading levels shown",
338
351
  "settings.density": "Outline density",
339
352
  "settings.density.comfy": "Comfy",
@@ -362,7 +375,6 @@ window.__ModuleLoader__.load({
362
375
  "settings.fuzzy": "Fuzzy search",
363
376
  "settings.hover": "Hover preview card",
364
377
  "settings.remember": "Remember the reading position",
365
- "settings.autoLoad": "Load history automatically",
366
378
  "settings.debug": "Print the mount diagnostic to the console",
367
379
  "settings.debug.tip": "The console then prints one mount line (host capabilities and jump-loader state)",
368
380
  "settings.overridden": "Customized",
@@ -435,11 +447,11 @@ window.__ModuleLoader__.load({
435
447
  // purpose — they describe THIS screen, not the account.
436
448
  var NAMESPACE = "dsh-quick-toc";
437
449
  // Fields backed by the Host settings document; everything else is localStorage.
438
- var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, density: 1, accent: 1, glass: 1, glassStrength: 1, sheetHeight: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
450
+ var HOST_FIELDS = { dock: 1, surface: 1, scrollbar: 1, lang: 1, levels: 1, density: 1, accent: 1, glass: 1, glassStrength: 1, sheetHeight: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
439
451
  // The schema defaults (mirrored here for the card's per-field reset and for the
440
452
  // one-time import of a 0.5.x install's stored values).
441
453
  var DEFAULTS = {
442
- dock: "left", y: 0, w: 0, h: 0,
454
+ dock: "left", surface: "both", scrollbar: "bar", y: 0, w: 0, h: 0,
443
455
  // "share" of see-through: 0 = nearly solid, 100 = the lightest material
444
456
  levels: [1, 2, 3, 4, 5, 6], density: "comfy", accent: "blue",
445
457
  glass: true, glassStrength: 60, sheetHeight: 1,
@@ -558,10 +570,24 @@ window.__ModuleLoader__.load({
558
570
  // colour, and the blur strings become undefined rather than empty.
559
571
  // A device that cannot hold the frame rate loses the blur by itself. The
560
572
  // verdict is remembered, so a slow machine pays for the probe once.
561
- var AUTO_OFF_KEY = "dsh-quick-toc.glassAutoOff.v1";
573
+ //
574
+ // v2: the v1 key was decided while the curtain's 0.5s ENTRANCE was being sampled,
575
+ // and the entrance composites the blurred sheet every frame by design. Measured in
576
+ // a sandbox profile with a clean store: the v1 verdict appeared after a curtain
577
+ // opening and never went away, which silently removed the blur from BOTH surfaces
578
+ // for good (the palette is shared). The material is now judged on the settled
579
+ // surface (see the probe), and the switch is a real way back (see the card):
580
+ // turning it on clears an automatic verdict. A new key lets a browser that already
581
+ // carries the old one start clean.
582
+ var AUTO_OFF_KEY = "dsh-quick-toc.glassAutoOff.v2";
562
583
  var autoGlassOff = (function () {
563
584
  try { return localStorage.getItem(AUTO_OFF_KEY) === "1"; } catch (e) { return false; }
564
585
  })();
586
+ // The reader asking for the material again outranks a measurement made for them.
587
+ var clearAutoGlassOff = function () {
588
+ autoGlassOff = false;
589
+ try { localStorage.removeItem(AUTO_OFF_KEY); } catch (e) {}
590
+ };
565
591
  var glassC = function (base, prefs) {
566
592
  var on = !!(prefs && prefs.glass) && !autoGlassOff && GLASS_OK;
567
593
  var s = prefs && prefs.glassStrength !== undefined ? prefs.glassStrength : 60;
@@ -587,6 +613,37 @@ window.__ModuleLoader__.load({
587
613
  out.sheetBlur = on ? "blur(5px) saturate(1.1)" : undefined;
588
614
  return out;
589
615
  };
616
+ // The ruler is a TAPE: every heading gets a slot at least `minGap` apart, and when
617
+ // the slots no longer fit the track the tape is TALLER than its window and slides
618
+ // with the scroll. This is the behaviour the reader asked for over a static map:
619
+ // the bars have to move as the outline scrolls, and a dense stretch has to look
620
+ // dense. A slot therefore does NOT claim the row's place in the content any more —
621
+ // the highlight is the slice of the tape the viewport mark covers, so the bars that
622
+ // light up always sit inside the mark and travel through it as the list scrolls.
623
+ // count headings on the tape; track usable lane height
624
+ // scrollTop/view/total the outline's viewport and content
625
+ function rulerLayout(count, total, track, scrollTop, view, minGap) {
626
+ var gap = count > 1 ? Math.max(minGap, track / (count - 1)) : 0;
627
+ var tapeH = Math.max(track, Math.round((count - 1) * gap) + 10);
628
+ var shift = Math.max(0, tapeH - track);
629
+ var room = Math.max(1, total - view);
630
+ var pos = Math.max(0, Math.min(1, scrollTop / room));
631
+ var share = total > 0 ? Math.min(1, view / total) : 1;
632
+ var markH = Math.max(28, Math.round(track * share));
633
+ var markTop = Math.max(0, Math.min(track - markH, Math.round((track - markH) * pos)));
634
+ var offset = shift * pos;
635
+ var canScroll = total - view > 4;
636
+ var ticks = [];
637
+ for (var i = 0; i < count; i++) {
638
+ var y = Math.round(i * gap) - offset;
639
+ ticks.push({
640
+ top: Math.round(i * gap),
641
+ // a 10px box: a bar whose centre is inside the mark counts as inside it
642
+ inView: canScroll && y >= markTop - 5 && y <= markTop + markH + 5
643
+ });
644
+ }
645
+ return { ticks: ticks, gap: gap, tapeH: tapeH, shift: shift, offset: offset };
646
+ }
590
647
  // Writing a field's schema default through the card IS its reset — a value that
591
648
  // equals the default carries no customization, so it must not pin an entry (and
592
649
  // the "customized" badge with it) into the user layer.
@@ -854,6 +911,9 @@ window.__ModuleLoader__.load({
854
911
  var handleNum = Number(v.handle);
855
912
  return {
856
913
  dock: v.dock === "right" ? "right" : "left",
914
+ // which views exist; anything unknown falls back to the historical "both"
915
+ surface: v.surface === "panel" || v.surface === "curtain" ? v.surface : "both",
916
+ scrollbar: v.scrollbar === "band" ? "band" : "bar",
857
917
  lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
858
918
  levels: levels || LEVELS_ALL.slice(),
859
919
  density: v.density === "compact" ? "compact" : "comfy",
@@ -876,7 +936,7 @@ window.__ModuleLoader__.load({
876
936
  var over = hostOverlay(hostSnap);
877
937
  if (!over) return local;
878
938
  return {
879
- dock: over.dock, y: local.y, w: local.w, h: local.h,
939
+ dock: over.dock, surface: over.surface, scrollbar: over.scrollbar, y: local.y, w: local.w, h: local.h,
880
940
  levels: over.levels, density: over.density, accent: over.accent,
881
941
  glass: over.glass, glassStrength: over.glassStrength, sheetHeight: over.sheetHeight,
882
942
  zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
@@ -2653,6 +2713,14 @@ window.__ModuleLoader__.load({
2653
2713
  programScrollRef.current = Date.now() + (ms || 250);
2654
2714
  };
2655
2715
  var lastScrollTopRef = react.useRef(0);
2716
+ // One page per ARRIVAL at the top: set when that page was asked for (by the
2717
+ // scroll's rising edge or by a wheel tick) and cleared once the list leaves the
2718
+ // top. Sitting at the top used to ask again on every wheel tick.
2719
+ var topLoadRef = react.useRef(false);
2720
+ // Set when the top asked the host for a page: the reveal waits for that page to
2721
+ // land instead of showing rows whose history is not here yet (see the effect
2722
+ // below the `groups` memo).
2723
+ var pendingRevealRef = react.useRef(false);
2656
2724
  var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
2657
2725
  // Auto-load bookkeeping (see the paging block): at most one pull in flight, a beat
2658
2726
  // between pulls, and a per-gesture budget so a runaway loop can never swallow the
@@ -3488,6 +3556,20 @@ window.__ModuleLoader__.load({
3488
3556
  // two comparisons disagree forever — the panel then republishes its default on
3489
3557
  // every pass and stomps a fresh change with the value it had just read back.
3490
3558
  var prefs = usePrefs();
3559
+ // ---- which surfaces exist ------------------------------------------------
3560
+ // 'both' is the historical behaviour. The other two do not merely prefer one
3561
+ // surface: they REMOVE the other, which is what was asked for (readers who only
3562
+ // ever use the curtain kept getting the docked panel's edge pill over the
3563
+ // transcript, and readers who only use the panel never wanted the droplet).
3564
+ var surface = prefs.surface === "curtain" ? "curtain" : (prefs.surface === "panel" ? "panel" : "both");
3565
+ var panelMode = surface !== "curtain";
3566
+ var curtainMode = surface !== "panel";
3567
+ // A preference change (or a document written elsewhere) must not leave the
3568
+ // removed surface on screen: close what is no longer offered.
3569
+ react.useEffect(function () {
3570
+ if (!curtainMode && sheetOpen) closeSheet();
3571
+ if (!panelMode && open) setOpen(false);
3572
+ }, [surface, sheetOpen, open]);
3491
3573
  // ---- the day strip (see its call site) -----------------------------------
3492
3574
  // Declared up here with the other hooks: an early "nothing to show yet"
3493
3575
  // return must not skip them, or the first empty render and every real one
@@ -3544,9 +3626,11 @@ window.__ModuleLoader__.load({
3544
3626
  measure();
3545
3627
  return undefined;
3546
3628
  }, [open, sheetOpen]);
3547
- var _sAutoGlass = react.useState(autoGlassOff);
3548
- var autoGlassState = _sAutoGlass[0];
3549
- var setAutoGlassOff = _sAutoGlass[1];
3629
+ // A revision, not a value: the probe's verdict must re-render the panel, while
3630
+ // the palette itself reads the module flag (so the card can clear it and the
3631
+ // next render — any preference write — picks the material back up).
3632
+ var _sGlassRev = react.useState(0);
3633
+ var bumpGlassRev = _sGlassRev[1];
3550
3634
  var _sSheetFrac = react.useState(null);
3551
3635
  var sheetDragFrac = _sSheetFrac[0];
3552
3636
  var setSheetDragFrac = _sSheetFrac[1];
@@ -3571,10 +3655,12 @@ window.__ModuleLoader__.load({
3571
3655
  setSheetDragFrac(null);
3572
3656
  };
3573
3657
  // ---- slow-device probe (see AUTO_OFF_KEY) --------------------------------
3574
- // Sampled once per curtain opening, and only while the material is on: the
3575
- // first 700ms of animated curtain is the heaviest thing this plugin does, so
3576
- // a p90 frame time over ~26ms there means the blur is costing more than the
3577
- // look is worth on this machine.
3658
+ // Sampled once per curtain opening, and only while the material is on. The
3659
+ // entrance is deliberately EXCLUDED: the 0.5s drop composites a blurred sheet
3660
+ // every frame by design, and sampling it is what switched the blur off in a
3661
+ // sandbox profile whose page then held the frame rate perfectly well. The
3662
+ // settled surface is what the reader actually lives with, so that is what gets
3663
+ // judged: 40 frames starting after the drop has landed.
3578
3664
  react.useEffect(function () {
3579
3665
  if (!sheetOpen || !prefs.glass || autoGlassOff) return undefined;
3580
3666
  if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return undefined;
@@ -3596,19 +3682,24 @@ window.__ModuleLoader__.load({
3596
3682
  if (p90 > 26) {
3597
3683
  autoGlassOff = true;
3598
3684
  try { localStorage.setItem(AUTO_OFF_KEY, "1"); } catch (e) {}
3599
- setAutoGlassOff(true);
3685
+ bumpGlassRev(function (n) { return n + 1; });
3600
3686
  }
3601
3687
  };
3602
- var id = window.requestAnimationFrame(tick);
3603
- var guard = setTimeout(finish, 1200);
3604
- return function () { stop = true; clearTimeout(guard); try { window.cancelAnimationFrame(id); } catch (e) {} };
3605
- }, [sheetOpen]);
3688
+ // 620ms = the 500ms entrance plus a margin for its own landing frame
3689
+ var start = setTimeout(function () {
3690
+ if (stop) return;
3691
+ last = 0;
3692
+ window.requestAnimationFrame(tick);
3693
+ }, 620);
3694
+ var guard = setTimeout(finish, 620 + 1200);
3695
+ return function () { stop = true; clearTimeout(start); clearTimeout(guard); };
3696
+ }, [sheetOpen, prefs.glass]);
3606
3697
  // The render's own palette: blue shares the module object itself (default
3607
3698
  // path: zero behaviour change), any other theme merges the same keys.
3608
3699
  // Every C.* use below this line — rows, chips, the accent bar, hit marks —
3609
3700
  // follows the preference without touching a single call site.
3610
3701
  var C = glassC(themeC(prefs.accent), {
3611
- glass: prefs.glass && !autoGlassState,
3702
+ glass: prefs.glass && !autoGlassOff,
3612
3703
  glassStrength: prefs.glassStrength
3613
3704
  });
3614
3705
  // the once-injected stylesheet cannot see per-render values: repaint its
@@ -4119,8 +4210,8 @@ window.__ModuleLoader__.load({
4119
4210
  autoLoadStep(el);
4120
4211
  }, fast ? AUTO_WAIT_MS : AUTO_PACE_MS);
4121
4212
  };
4122
- var autoLoadStep = function (el) {
4123
- if (!autoLoadRef.current || !el || !el.isConnected) return false;
4213
+ var autoLoadStep = function (el, force) {
4214
+ if ((!autoLoadRef.current && !force) || !el || !el.isConnected) return false;
4124
4215
  if (pullBusyRef.current) return true; // a pull is in flight: still progressing
4125
4216
  var now = Date.now();
4126
4217
  if (now - autoStampRef.current > AUTO_BURST_IDLE_MS) autoBurstRef.current = 0;
@@ -4180,19 +4271,24 @@ window.__ModuleLoader__.load({
4180
4271
  };
4181
4272
  var loadOlderOutline = function (el) {
4182
4273
  var grow = function () {
4183
- if (visibleCount < groups.length) {
4184
- // Revealing older rows prepends them, so the reader's content is pushed down.
4185
- // Sample first and let the layout effect take the push out again in the same
4186
- // frame: correcting two frames later (the old double-rAF) IS the bounce the
4187
- // reader saw when they reached the unloaded part of the list.
4188
- holdPlace(el);
4189
- setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
4190
- // the revealed rows are not in the DOM yet, so re-check once after the render:
4191
- // if the reveal uncovered unloaded turns, they load within the same gesture
4192
- autoRetry();
4193
- return true;
4194
- }
4195
- return false;
4274
+ if (visibleCount >= groups.length) return false;
4275
+ // Reveal only rows that are already LOADED. A placeholder would render as
4276
+ // 未加载, and the reader's rule for this gesture is "one page, like the host's
4277
+ // own 加载更早" — the index must not run ahead of the history behind it.
4278
+ var top = groups.length - visibleCount;
4279
+ var run = 0;
4280
+ while (run < PAGE_SIZE && top - 1 - run >= 0 && groups[top - 1 - run] && !groups[top - 1 - run].ghost) run++;
4281
+ if (!run) return false;
4282
+ // Revealing older rows prepends them, so the reader's content is pushed down.
4283
+ // Sample first and let the layout effect take the push out again in the same
4284
+ // frame: correcting two frames later (the old double-rAF) IS the bounce the
4285
+ // reader saw when they reached the unloaded part of the list.
4286
+ holdPlace(el);
4287
+ setVisibleCount(Math.min(groups.length, visibleCount + run));
4288
+ // the revealed rows are not in the DOM yet, so re-check once after the render:
4289
+ // if the reveal uncovered unloaded turns, they load within the same gesture
4290
+ autoRetry();
4291
+ return true;
4196
4292
  };
4197
4293
  var viaHostButton = function () { return viaHostButtonRef.current(); };
4198
4294
  // In SEARCH mode the list shows matches, not the outline: paging the outline's
@@ -4206,10 +4302,17 @@ window.__ModuleLoader__.load({
4206
4302
  if (pulled) confirmPlace(el);
4207
4303
  return pulled || grow();
4208
4304
  }
4209
- // Outline mode: reveal more of the index, and let the auto-loader fetch the
4210
- // history behind the gap the reader has scrolled into (see autoLoadStep).
4305
+ // Outline mode: reveal the next slice of the index and pull exactly ONE host
4306
+ // page — the same `loadOlder()` the host's own 加载更早 button calls. This used
4307
+ // to run the auto-loader here, whose "cover the ghost you reached" path pages up
4308
+ // to 200 messages a hop: one wheel arrival could therefore load several times
4309
+ // what the button loads and leave the outline full of 未加载 rows.
4211
4310
  var grew = grow();
4212
- var fetched = autoLoadStep(el);
4311
+ var fetched = pullPage() || viaHostButton();
4312
+ // Nothing loaded above the window (the frontier): wait for the page to land and
4313
+ // reveal its turns then. When the index COULD advance, the jump above already
4314
+ // moved the reader off the top, which re-arms the gesture by itself.
4315
+ if (fetched && !grew) pendingRevealRef.current = true;
4213
4316
  return grew || fetched;
4214
4317
  };
4215
4318
 
@@ -4249,7 +4352,13 @@ window.__ModuleLoader__.load({
4249
4352
  }
4250
4353
  if (hint === "more") setHint(""); // scrolling up is what the hint asked for
4251
4354
  if (el.scrollTop <= 1) {
4252
- var progressed = loadOlderOutline(el);
4355
+ // ONE page per arrival, exactly like the host's own 加载更多: a spin used to
4356
+ // reveal another PAGE_SIZE of index rows on EVERY tick, so one gesture could
4357
+ // uncover far more turns than were loaded and leave 未加载 rows behind. The
4358
+ // flag is cleared when the list leaves the top (see onListScroll).
4359
+ var alreadyLoaded = topLoadRef.current;
4360
+ topLoadRef.current = true;
4361
+ var progressed = alreadyLoaded ? false : loadOlderOutline(el);
4253
4362
  if (!progressed) {
4254
4363
  if (query.trim() !== "") {
4255
4364
  if (hint !== "oldest") setHint("oldest");
@@ -4267,6 +4376,165 @@ window.__ModuleLoader__.load({
4267
4376
  };
4268
4377
 
4269
4378
  // scroll to the top edge also loads older groups (keeps the visual position)
4379
+ // ---- the scroll-position indicator ---------------------------------------
4380
+ // A density ruler plus a viewport marker on the list's right edge.
4381
+ // * the TICKS are a TAPE: one slot per H1/H2, at least 7px apart, and when the
4382
+ // slots overflow the window the tape slides with the scroll (see
4383
+ // rulerLayout). Clicking one jumps to its heading.
4384
+ // * the MARKER is where the reader is: a short vertical bar ("bar") or a soft
4385
+ // band ("band"). Its height is the viewport's share of the content; the bars
4386
+ // under it are the ones painted in the theme colour.
4387
+ // It must live OUTSIDE the scroller: the list carries a top/bottom fade mask,
4388
+ // and anything drawn inside it is eaten at the ends (the "clipped, odd" look).
4389
+ var barHostRef = react.useRef(null);
4390
+ var barThumbRef = react.useRef(null);
4391
+ var barTapeRef = react.useRef(null);
4392
+ var barDragRef = react.useRef(null);
4393
+ var _sBarHot = react.useState(false);
4394
+ var barHot = _sBarHot[0];
4395
+ var setBarHot = _sBarHot[1];
4396
+ // The bottom needs room for the panel's 12px corner AND the marker's 5px glow,
4397
+ // or the end of the mark gets shaved off exactly when it reaches the bottom.
4398
+ var BAR_BOTTOM_GAP = 20;
4399
+ var BAR_INSET_TOP = 10;
4400
+ var TICK_MIN_GAP = 7; // the ruler stops getting denser than this
4401
+ var barMode = prefs.scrollbar === "band" ? "band" : "bar";
4402
+ // the marker lives in its own lane, never on top of the rows: the list keeps
4403
+ // this much padding on its right so text and the current-group box stop short
4404
+ // The curtain has room, so the whole ruler scales up with the marker: the lane
4405
+ // widens, the ticks get longer and the frame grows with them.
4406
+ var barScale = sheetOpen ? 1.6 : 1;
4407
+ var barTickW = Math.round(10 * barScale);
4408
+ var barLane = (barMode === "band" ? 22 : 24) + (sheetOpen ? 8 : 0);
4409
+ // The air between the list's content edge and the lane. The current-group box
4410
+ // bleeds 5px into it (its own -5px side margin), so the box the reader sees sits
4411
+ // ~13px from the band — two tinted frames needed room, they were nearly touching.
4412
+ var BAR_TEXT_GAP = 18;
4413
+ // where the marker currently sits, written straight to the DOM: a re-render per
4414
+ // scroll frame would rebuild the whole outline
4415
+ // One pass over the scroll position: the tape (the ruler) slides, and the marker
4416
+ // (a rounded bar, or the band that wraps the ruler) follows.
4417
+ var layoutScrollBar = function (el) {
4418
+ if (typeof layoutBarLane === "function") layoutBarLane();
4419
+ var host = barHostRef.current;
4420
+ var thumb = barThumbRef.current;
4421
+ var tape = barTapeRef.current;
4422
+ if (!host || !el) return;
4423
+ var view = el.clientHeight || 0;
4424
+ var total = el.scrollHeight || 0;
4425
+ var laneH = host.clientHeight || 0;
4426
+ var track = Math.max(40, laneH - BAR_INSET_TOP - BAR_BOTTOM_GAP);
4427
+ var share = total > 0 ? Math.min(1, view / total) : 1;
4428
+ var scrollTop = el.scrollTop || 0;
4429
+ var room = Math.max(1, total - view);
4430
+ var pos = Math.max(0, Math.min(1, scrollTop / room));
4431
+ var canScroll = total - view > 4;
4432
+ // The ruler: one slot per heading, at least TICK_MIN_GAP apart. When the slots
4433
+ // overflow the window the tape is taller and slides with the scroll — the bars
4434
+ // move as the outline moves, and a dense stretch stays dense.
4435
+ if (tape && tape.style) {
4436
+ var kids = tape.children || [];
4437
+ var laid = rulerLayout(kids.length, total, track, scrollTop, view, TICK_MIN_GAP);
4438
+ for (var ti = 0; ti < kids.length; ti++) {
4439
+ var kid = kids[ti];
4440
+ var g = laid.ticks[ti];
4441
+ if (!kid || !kid.style || !g) continue;
4442
+ kid.style.display = "";
4443
+ kid.style.top = g.top + "px"; // the slot; the 10px box centres its bar 5px in
4444
+ var bar = kid.firstElementChild || (kid.children && kid.children[0]);
4445
+ if (bar && bar.style) {
4446
+ bar.style.background = g.inView ? C.accent : "rgba(128,128,128,0.32)";
4447
+ bar.style.opacity = g.inView ? "0.95" : "0.8";
4448
+ }
4449
+ }
4450
+ tape.style.height = laid.tapeH + "px";
4451
+ tape.style.transform = "translateY(" + Math.round(-laid.offset) + "px)";
4452
+ }
4453
+ if (!thumb || track <= 8) return;
4454
+ // The mark is the viewport's share of the content: its height is that share and
4455
+ // its travel is the scroll's. (With share*track < the 28px grab floor the mark
4456
+ // keeps its grab size and the bottom end gives way.)
4457
+ var markHeight = Math.max(28, Math.round(track * share));
4458
+ var markTop = Math.max(0, Math.min(track - markHeight, Math.round((track - markHeight) * pos)));
4459
+ thumb.style.height = markHeight + "px";
4460
+ // the element's own top already carries BAR_INSET_TOP (see its style), so the
4461
+ // transform adds only the travel — one inset, or the mark sits 10px low
4462
+ thumb.style.transform = "translateY(" + markTop + "px)";
4463
+ thumb.style.opacity = canScroll ? "1" : "0.3";
4464
+ thumb.style.pointerEvents = canScroll ? "auto" : "none";
4465
+ };
4466
+ // The lane is an in-flow flex column beside the list, so there is nothing to
4467
+ // position by hand: this stays as a no-op because layoutScrollBar calls it.
4468
+ var layoutBarLane = function () {};
4469
+ var onBarDown = function (e) {
4470
+ var el = listRef.current;
4471
+ if (!el) return;
4472
+ e.preventDefault();
4473
+ e.stopPropagation();
4474
+ // A drag is measured in CONTENT space, fixed at the moment it starts: the
4475
+ // pointer's travel maps to the content that existed then, so history arriving
4476
+ // underneath can never accelerate the reader towards the top.
4477
+ var host0 = barHostRef.current;
4478
+ var track0 = Math.max(1, (host0 ? host0.clientHeight : 0) - BAR_INSET_TOP - BAR_BOTTOM_GAP);
4479
+ var thumbH0 = barThumbRef.current ? barThumbRef.current.offsetHeight || 22 : 22;
4480
+ barDragRef.current = {
4481
+ moved: false,
4482
+ startY: e.clientY,
4483
+ startTop: el.scrollTop || 0,
4484
+ room0: Math.max(1, el.scrollHeight - el.clientHeight),
4485
+ travel0: Math.max(1, track0 - thumbH0)
4486
+ };
4487
+ setBarHot(true);
4488
+ if (e.currentTarget && e.pointerId !== undefined && e.currentTarget.setPointerCapture) {
4489
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* no capture: fine */ }
4490
+ }
4491
+ };
4492
+ var onBarMove = function (e) {
4493
+ var d = barDragRef.current;
4494
+ var el = listRef.current;
4495
+ if (!d || !el) return;
4496
+ var dy = e.clientY - d.startY;
4497
+ if (Math.abs(dy) > 2) d.moved = true;
4498
+ // Reached the top: stand still until the page that arrival asked for lands.
4499
+ // The pointer turning back DOWN is an ordinary drag, though — keeping the hold
4500
+ // there latched the mark at the top until the button was released.
4501
+ if (d.hold) {
4502
+ if (e.clientY > d.holdY + 1) d.hold = false;
4503
+ else if (el.scrollHeight <= d.holdHeight) return;
4504
+ else d.hold = false;
4505
+ }
4506
+ var want = d.startTop + (dy / d.travel0) * d.room0;
4507
+ var room = Math.max(1, el.scrollHeight - el.clientHeight);
4508
+ var next = Math.max(0, Math.min(room, want));
4509
+ if (next <= 0 && dy < 0) {
4510
+ if (!d.hold) { d.hold = true; d.holdY = e.clientY; d.holdHeight = el.scrollHeight; }
4511
+ }
4512
+ el.scrollTop = Math.round(next);
4513
+ };
4514
+ var onBarUp = function (e) {
4515
+ var d = barDragRef.current;
4516
+ barDragRef.current = null;
4517
+ setBarHot(false);
4518
+ if (e && e.currentTarget && e.currentTarget.releasePointerCapture && e.pointerId !== undefined) {
4519
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
4520
+ }
4521
+ // a press that did not drag centres the view on the pointer instead
4522
+ if (d && !d.moved) {
4523
+ var el = listRef.current;
4524
+ var host = barHostRef.current;
4525
+ var thumb = barThumbRef.current;
4526
+ if (el && host && thumb) {
4527
+ var rect = host.getBoundingClientRect();
4528
+ // the same band the mark travels in (see layoutScrollBar): the lane's top
4529
+ // inset belongs to both ends, or a press lands the view a hair off
4530
+ var track = Math.max(1, (host.clientHeight || 0) - BAR_INSET_TOP - BAR_BOTTOM_GAP);
4531
+ var thumbH = thumb.offsetHeight || 22;
4532
+ var y = Math.max(0, Math.min(track - thumbH, e.clientY - rect.top - BAR_INSET_TOP - thumbH / 2));
4533
+ var room = Math.max(1, el.scrollHeight - el.clientHeight);
4534
+ el.scrollTop = Math.round((y / Math.max(1, track - thumbH)) * room);
4535
+ }
4536
+ }
4537
+ };
4270
4538
  var onListScroll = function (e) {
4271
4539
  // A scroll the panel caused itself (see markProgramScroll) carries no
4272
4540
  // reader intent: it still updates the bottom state and the paging anchor,
@@ -4283,7 +4551,12 @@ window.__ModuleLoader__.load({
4283
4551
  if (hint === "more") setHint("");
4284
4552
  if (hoverCard) hoverEnd(true);
4285
4553
  }
4554
+ var wasAbove24 = lastScrollTopRef.current > 24;
4286
4555
  lastScrollTopRef.current = el.scrollTop;
4556
+ // leaving the top re-arms the one-page gate (see onListWheel): a later
4557
+ // arrival is a new request, staying here is not
4558
+ if (el.scrollTop > 24) topLoadRef.current = false;
4559
+ layoutScrollBar(el);
4287
4560
  if (!programmatic) syncBackBtn(el); // reached the remembered place by hand? the button is done
4288
4561
  // Remember what the reader is looking at. Sampling on every scroll event is what
4289
4562
  // lets a change the reader did NOT start — the host extending the window by itself
@@ -4294,22 +4567,21 @@ window.__ModuleLoader__.load({
4294
4567
  // trigger any more: on any upward movement, ask the auto-loader whether the
4295
4568
  // unloaded stretch is coming into range (it decides, see frontierApproaching).
4296
4569
  // Throttled, because that probe reads geometry.
4297
- if (upward && el.scrollTop > 24 && Date.now() - autoProbeRef.current > 120) {
4298
- autoProbeRef.current = Date.now();
4299
- autoLoadStep(el);
4300
- }
4570
+ // no automatic paging any more: the reader asks by reaching the top
4301
4571
  if (el.scrollTop <= 24) {
4302
4572
  // Reaching the top is the ONLY place the exhausted case can be told
4303
4573
  // apart from "keep scrolling": after a search the list is parked at the
4304
4574
  // bottom, so the first wheel-up merely scrolls and never gets here.
4305
- var progressed = loadOlderOutline(el);
4575
+ var atTopFresh = wasAbove24 && !barDragRef.current && (!programmatic || query.trim() !== "");
4576
+ // the outline window grows only on that fresh arrival too: sitting at the top used
4577
+ // to keep revealing ghost pages, which looked like a flood of 未加载 rows
4578
+ if (atTopFresh) topLoadRef.current = true;
4579
+ var progressed = atTopFresh ? loadOlderOutline(el) : false;
4306
4580
  if (!progressed && upward) {
4307
4581
  if (query.trim() !== "") {
4308
4582
  if (hint !== "oldest") setHint("oldest");
4309
4583
  } else if (atHistoryStart()) {
4310
4584
  showBanner(T("search.hintOldest"));
4311
- } else if (!prefs.autoLoad) {
4312
- showBanner(T("hint.autoOff"));
4313
4585
  }
4314
4586
  }
4315
4587
  }
@@ -4570,6 +4842,20 @@ window.__ModuleLoader__.load({
4570
4842
  // render (the "did that turn actually page in?" verdict below).
4571
4843
  var groupsLiveRef = react.useRef(groups);
4572
4844
  groupsLiveRef.current = groups;
4845
+ // A top arrival asked the host for one page: when that page LANDs, extend the
4846
+ // window over the turns that just became real (never over a placeholder). The
4847
+ // reader sees exactly what loaded, and the wheel does not run ahead of the data.
4848
+ react.useEffect(function () {
4849
+ if (!pendingRevealRef.current) return;
4850
+ var top = groups.length - visibleCount;
4851
+ var run = 0;
4852
+ while (run < PAGE_SIZE && top - 1 - run >= 0 && groups[top - 1 - run] && !groups[top - 1 - run].ghost) run++;
4853
+ if (!run) return; // the page has not landed yet
4854
+ pendingRevealRef.current = false;
4855
+ var el = listRef.current;
4856
+ if (el) holdPlace(el);
4857
+ setVisibleCount(Math.min(groups.length, visibleCount + run));
4858
+ }, [groups, visibleCount]);
4573
4859
  react.useEffect(function () {
4574
4860
  return function () {
4575
4861
  if (keepPlaceRafRef.current) cancelAnimationFrame(keepPlaceRafRef.current);
@@ -4636,6 +4922,189 @@ window.__ModuleLoader__.load({
4636
4922
  var shownGroups = groups.slice(Math.max(0, groups.length - visibleCount));
4637
4923
  var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
4638
4924
 
4925
+ // the ticks: one per H1/H2/H3 entry the list actually renders. The window
4926
+ // starts at the first shown group, so the ruler never claims a place for a
4927
+ // row that is not in the DOM (the reader can see every tick it draws).
4928
+ var barFirstGi = Math.max(0, groups.length - visibleCount);
4929
+ var barTicks = react.useMemo(function () {
4930
+ var all = [];
4931
+ for (var k = 0; k < shownGroups.length; k++) {
4932
+ var gi = barFirstGi + k;
4933
+ var g = shownGroups[k];
4934
+ if (!g || g.ghost || !g.headings) continue;
4935
+ var added = 0;
4936
+ for (var hi = 0; hi < g.headings.length; hi++) {
4937
+ var h = g.headings[hi];
4938
+ var lv = Math.min(6, Math.max(1, h.level || 1));
4939
+ if (lv > 3) continue; // H4 and deeper stay out of the ruler
4940
+ all.push({ key: h.key, idx: h.idx, gi: gi, level: lv, title: h.title || "" });
4941
+ added++;
4942
+ }
4943
+ // keep at least one tick per turn that has any H1/H2
4944
+ if (!added && g.headings.length) {
4945
+ var first = g.headings[0];
4946
+ all.push({ key: first.key, idx: first.idx, gi: gi, level: Math.min(6, Math.max(1, first.level || 1)), title: first.title || "" });
4947
+ }
4948
+ }
4949
+ return all;
4950
+ }, [shownGroups, barFirstGi]);
4951
+ // A very long window can hold thousands of headings. Up to BAR_CAP every heading
4952
+ // gets its own slot (the tape grows instead of dropping any); beyond it the set is
4953
+ // sampled evenly by index, because a DOM node per heading rewritten on every
4954
+ // scroll frame is what makes the lane expensive.
4955
+ var BAR_CAP = 400;
4956
+ var barSlice = barTicks;
4957
+ if (barTicks.length > BAR_CAP) {
4958
+ var thinStep = barTicks.length / BAR_CAP;
4959
+ var keptTicks = [];
4960
+ for (var ck = 0; ck < BAR_CAP; ck++) keptTicks.push(barTicks[Math.min(barTicks.length - 1, Math.round(ck * thinStep))]);
4961
+ barSlice = keptTicks;
4962
+ }
4963
+ // The tape is laid out from the rendered window only (barSlice, capped above): the
4964
+ // layout pass gives every heading a slot at least TICK_MIN_GAP apart, and the tape
4965
+ // slides inside its clipped window when the slots do not all fit. Nothing here
4966
+ // reads row geometry any more, so a scroll never forces a layout read.
4967
+ react.useLayoutEffect(function () {
4968
+ layoutScrollBar(listRef.current);
4969
+ }, [open, sheetOpen, barMode, groups.length, visibleCount, shownGroups.length, activeGroup,
4970
+ sheetColW, marksCol, panelW, panelH, prefs.density, levels, barTicks]);
4971
+ var scrollBarEl = groups.length ? react_jsx_runtime.jsx("div", {
4972
+ ref: barHostRef,
4973
+ className: "dqt-bar",
4974
+ "data-dqt-bar": barMode,
4975
+ style: {
4976
+ // An in-flow column immediately right of the list: when the search or
4977
+ // bookmarks column is pushed in, the list gives up width and the lane
4978
+ // follows by ordinary layout — no percentages, no stray overlap.
4979
+ position: "relative",
4980
+ flex: "0 0 " + barLane + "px",
4981
+ alignSelf: "stretch",
4982
+ width: barLane,
4983
+ // The lane sits right beside the outline and nearly on the panel's own rim:
4984
+ // the reader asked for the mark to hug both sides, in the panel and across
4985
+ // the curtain alike (this is the ONE gap constant for the lane).
4986
+ marginRight: Math.round(4 * barScale),
4987
+ boxSizing: "border-box",
4988
+ zIndex: 3,
4989
+ pointerEvents: "none"
4990
+ },
4991
+ children: [
4992
+ // the ruler: a clipped window over the sliding tape
4993
+ react_jsx_runtime.jsx("div", {
4994
+ key: "tapewrap",
4995
+ style: {
4996
+ position: "absolute",
4997
+ top: BAR_INSET_TOP,
4998
+ bottom: BAR_BOTTOM_GAP,
4999
+ left: barMode === "band" ? 5 : 0,
5000
+ width: barTickW,
5001
+ // the slots are laid out inside this window; a tape taller than it slides
5002
+ // behind this clip, exactly like a scrollbar's track
5003
+ overflow: "hidden"
5004
+ },
5005
+ children: react_jsx_runtime.jsx("div", {
5006
+ ref: barTapeRef,
5007
+ "data-dqt-tape": "on",
5008
+ "data-dqt-ticks": String(barSlice.length),
5009
+ style: { position: "absolute", left: 0, top: 0, width: barTickW, height: "100%" },
5010
+ children: barSlice.map(function (t, ti) {
5011
+ var w = Math.round((t.level === 1 ? 10 : (t.level === 2 ? 6 : 3)) * barScale); // H1 long, H2 medium, H3 a dot
5012
+ return react_jsx_runtime.jsx("div", {
5013
+ "data-dqt-tick": String(t.level),
5014
+ "data-dqt-tick-pos": String(ti),
5015
+ title: t.title,
5016
+ onClick: function () { jump(t.key, t.idx); },
5017
+ style: {
5018
+ position: "absolute",
5019
+ left: 0,
5020
+ top: 0, // placed by layoutScrollBar: one slot per heading
5021
+ height: 10,
5022
+ width: barTickW,
5023
+ display: "flex",
5024
+ alignItems: "center",
5025
+ justifyContent: "flex-start",
5026
+ pointerEvents: "auto",
5027
+ cursor: "pointer"
5028
+ },
5029
+ children: react_jsx_runtime.jsx("span", {
5030
+ style: {
5031
+ display: "block",
5032
+ width: w,
5033
+ height: 2,
5034
+ borderRadius: 1,
5035
+ // neutral until layoutScrollBar knows which rows are in view
5036
+ background: "rgba(128,128,128,0.32)",
5037
+ opacity: 0.8
5038
+ }
5039
+ })
5040
+ }, "tick-" + ti);
5041
+ })
5042
+ })
5043
+ }),
5044
+ // the marker: a rounded bar (wider in the curtain) or a frame around the ruler
5045
+ react_jsx_runtime.jsx("div", {
5046
+ key: "thumb",
5047
+ ref: barThumbRef,
5048
+ "data-dqt-barthumb": barMode,
5049
+ onPointerDown: onBarDown,
5050
+ onPointerMove: onBarMove,
5051
+ onPointerUp: onBarUp,
5052
+ onPointerCancel: onBarUp,
5053
+ onMouseEnter: function () { setBarHot(true); },
5054
+ onMouseLeave: function () { if (!barDragRef.current) setBarHot(false); },
5055
+ style: barMode === "band"
5056
+ ? {
5057
+ position: "absolute",
5058
+ left: 0,
5059
+ // a 2px gap to the panel's own rim: the band hugs the right edge, as
5060
+ // close as its 1px outline allows
5061
+ right: 2,
5062
+ top: BAR_INSET_TOP,
5063
+ height: 60,
5064
+ borderRadius: 7,
5065
+ // the same material as the current-turn box on the left, so the two
5066
+ // read as one family
5067
+ background: barHot ? "rgba(79,140,255,0.16)" : "rgba(79,140,255,0.10)",
5068
+ boxShadow: barHot ? "inset 0 0 0 1px rgba(79,140,255,0.65)" : "inset 0 0 0 1px rgba(79,140,255,0.40)",
5069
+ // the frame does not grow on hover: it becomes more solid instead
5070
+ transition: "background-color 0.14s ease, box-shadow 0.14s ease",
5071
+ pointerEvents: "auto",
5072
+ cursor: "pointer",
5073
+ touchAction: "none"
5074
+ }
5075
+ : {
5076
+ position: "absolute",
5077
+ right: 4,
5078
+ top: BAR_INSET_TOP,
5079
+ width: 12,
5080
+ height: 24,
5081
+ display: "flex",
5082
+ alignItems: "stretch",
5083
+ justifyContent: "flex-end",
5084
+ pointerEvents: "auto",
5085
+ cursor: "pointer",
5086
+ touchAction: "none"
5087
+ },
5088
+ children: barMode === "band" ? null : react_jsx_runtime.jsx("span", {
5089
+ "data-dqt-barvis": "bar",
5090
+ style: {
5091
+ display: "block",
5092
+ // wide enough that the rounded caps are actually visible, and the
5093
+ // curtain gets a thicker one; hovering makes it thicker still
5094
+ width: (sheetOpen ? 6 : 4) + (barHot ? 2 : 0),
5095
+ height: "100%",
5096
+ borderRadius: 999,
5097
+ background: C.accent,
5098
+ opacity: 0.95,
5099
+ boxShadow: barHot ? "0 0 0 5px rgba(79,140,255,0.10)" : "none",
5100
+ transition: "width 0.14s ease, box-shadow 0.14s ease"
5101
+ }
5102
+ })
5103
+ })
5104
+ ]
5105
+ }) : null;
5106
+
5107
+
4639
5108
  // the group currently being read (the turn under the middle of the
4640
5109
  // CONVERSATION viewport) stays bright in the outline; others are dimmed.
4641
5110
  // Every node key of every loaded group -> its group index: mapping ONLY the
@@ -5234,7 +5703,7 @@ window.__ModuleLoader__.load({
5234
5703
  rememberRef.current = !!prefs.remember;
5235
5704
  // the auto-loader's switch, mirrored for the same reason: its retry fires from an
5236
5705
  // older render's closure, and turning the setting off must stop it immediately
5237
- autoLoadRef.current = !!prefs.autoLoad;
5706
+ autoLoadRef.current = false; // manual only
5238
5707
  // Is any of the outline actually on screen? The follow scan is cheap now, but while
5239
5708
  // the panel sits collapsed there is no point measuring at scroll-frame rate.
5240
5709
  panelVisibleRef.current = !!(open || sheetOpen);
@@ -7165,14 +7634,16 @@ window.__ModuleLoader__.load({
7165
7634
  style: {
7166
7635
  position: "absolute",
7167
7636
  left: 0,
7168
- // the curtain's search column keeps its own strip: the overlay covers the
7169
- // outline, not the hits
7170
- right: sheetOpen && sheetColW > 0 ? sheetColW : 0,
7637
+ // the overlay lives INSIDE the pane now, so its right edge must step over
7638
+ // the lane: the bookmarks stop where the outline's rows stop
7639
+ right: barLane + Math.round(4 * barScale),
7171
7640
  top: 0,
7172
7641
  bottom: 0,
7173
7642
  overflowY: "auto",
7174
7643
  overflowAnchor: "none",
7175
- padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
7644
+ // the gap between those rows and the lane (BAR_TEXT_GAP), never the whole
7645
+ // lane width — the lane is a real flex column
7646
+ padding: sheetOpen ? ("0 " + BAR_TEXT_GAP + "px 8px 8px") : ("0 " + BAR_TEXT_GAP + "px 6px 6px"),
7176
7647
  direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
7177
7648
  },
7178
7649
  children: react_jsx_runtime.jsx("div", { style: { direction: "ltr" }, children: markRowsEl })
@@ -7207,7 +7678,11 @@ window.__ModuleLoader__.load({
7207
7678
  // block, so a padded scroll container would hold the pinned group
7208
7679
  // header that many pixels below the toolbar (a visible gap). Spacing
7209
7680
  // between groups comes from the per-group divider instead.
7210
- padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
7681
+ // the right padding is the indicator's lane (see barLane above): rows and the
7682
+ // current-group box stop short of it, in the outline and in the marks overlay
7683
+ // the lane is a real flex column now, so the list only needs the air between
7684
+ // its text and that column (BAR_TEXT_GAP), not the whole lane width
7685
+ padding: sheetOpen ? ("0 " + BAR_TEXT_GAP + "px 8px 8px") : ("0 " + BAR_TEXT_GAP + "px 6px 6px"),
7211
7686
  // the rows dissolve at BOTH edges instead of being cut by a hard line. The
7212
7687
  // top fade is safe now that the day strip lives outside the scroller: it
7213
7688
  // thins the rows approaching the strip, never the label itself.
@@ -7215,15 +7690,9 @@ window.__ModuleLoader__.load({
7215
7690
  WebkitMaskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
7216
7691
  flex: "1 1 auto",
7217
7692
  minHeight: 0,
7218
- // across the curtain the list does not touch the edges: it is a centred
7219
- // column with room at both sides, which is also what makes it slide left
7220
- // when the search column is pushed in
7221
- width: sheetOpen ? "100%" : undefined,
7222
- maxWidth: sheetOpen ? "90%" : undefined,
7223
- margin: sheetOpen ? "0 auto" : undefined,
7224
- // the 20px side padding sits INSIDE the 90% (content-box would make the
7225
- // rendered column 90% + 40px, i.e. ~93% of the curtain)
7226
- boxSizing: sheetOpen ? "border-box" : undefined,
7693
+ // across the curtain the WIDTH and centring belong to the pane that holds
7694
+ // this list AND its lane (see bodyRowEl): the lane has to travel with the
7695
+ // outline, or it sits way off at the sheet's edge instead of beside the rows
7227
7696
  // the scrollbar follows the dock side in the narrow panel; across the
7228
7697
  // curtain it sits on the list's right edge
7229
7698
  direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
@@ -7334,12 +7803,35 @@ window.__ModuleLoader__.load({
7334
7803
  // would go on showing the last day the outline was scrolled to
7335
7804
  children: onlyMarks ? "" : dayStripText
7336
7805
  });
7337
- var bodyRowEl = react_jsx_runtime.jsx("div", {
7338
- className: "dqt-body-row",
7806
+ // The outline AND its lane travel together. Across the curtain the pair is one
7807
+ // centred column (90% of the sheet at most): centring the LIST alone used to
7808
+ // leave the lane parked at the sheet's edge, nowhere near the rows it describes,
7809
+ // and opening the search column only looked closer because everything shrank.
7810
+ // In the docked panel the pane is simply the full row, so the lane sits right
7811
+ // beside the outline there too.
7812
+ var outlinePaneEl = react_jsx_runtime.jsx("div", {
7813
+ className: "dqt-outline-pane",
7339
7814
  // `position: relative` is what lets the bookmark overlay cover the list
7340
7815
  // without becoming part of its scrolled content
7816
+ style: {
7817
+ position: "relative",
7818
+ flex: "1 1 auto",
7819
+ minWidth: 0,
7820
+ minHeight: 0,
7821
+ display: "flex",
7822
+ flexDirection: "row",
7823
+ alignItems: "stretch",
7824
+ width: sheetOpen ? "100%" : undefined,
7825
+ maxWidth: sheetOpen ? "90%" : undefined,
7826
+ margin: sheetOpen ? "0 auto" : undefined,
7827
+ boxSizing: sheetOpen ? "border-box" : undefined
7828
+ },
7829
+ children: [listEl, marksOverlayEl, scrollBarEl]
7830
+ });
7831
+ var bodyRowEl = react_jsx_runtime.jsx("div", {
7832
+ className: "dqt-body-row",
7341
7833
  style: { position: "relative", flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
7342
- children: [listEl, marksOverlayEl, sheetSearchColEl]
7834
+ children: [outlinePaneEl, sheetSearchColEl]
7343
7835
  });
7344
7836
  // one geometry for the panel and its shadow twin, merged (never duplicated)
7345
7837
  var mergeStyle = function (base, extra) {
@@ -7777,7 +8269,7 @@ window.__ModuleLoader__.load({
7777
8269
  // a narrow conversation column leaves no room for a handle floating over the text:
7778
8270
  // below the threshold it slides back into the divider (its usual motion), while the
7779
8271
  // droplet above the conversation stays put as the way in
7780
- var edgeShown = !open && handleShown && chatViewActive && !narrowWindow;
8272
+ var edgeShown = !open && panelMode && handleShown && chatViewActive && !narrowWindow;
7781
8273
  // The pill is 12px centred in the 26px box, so its edge sits 7px inside the box
7782
8274
  // edge. Left dock: the panel opens at `area + 8`, so parking the box 1px out puts
7783
8275
  // the pill's left edge exactly on the panel's own left edge — one line.
@@ -7789,7 +8281,7 @@ window.__ModuleLoader__.load({
7789
8281
  // than two lines nobody sees at the same time.
7790
8282
  var edgeLineLeft = (viewport ? viewport.left : 0) + 1;
7791
8283
  var edgeLineRight = (viewport ? viewport.right + 52 : 60); // clear of the milestone rail
7792
- var edgeEl = (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
8284
+ var edgeEl = panelMode && (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
7793
8285
  style: {
7794
8286
  position: "fixed",
7795
8287
  width: EDGE_W,
@@ -7893,7 +8385,7 @@ window.__ModuleLoader__.load({
7893
8385
  // 140 wide and only 13.5 tall: the reader asked for a wider, flatter droplet
7894
8386
  // (it started at 92, then 120).
7895
8387
  var DROP_W = 140;
7896
- var topHandleEl = (!sheetOpen || !sheetHandleGone) && chatViewActive ? react_jsx_runtime.jsx("div", {
8388
+ var topHandleEl = curtainMode && (!sheetOpen || !sheetHandleGone) && chatViewActive ? react_jsx_runtime.jsx("div", {
7897
8389
  className: "dqt-sheet-handle",
7898
8390
  "data-sheet-handle": "on",
7899
8391
  style: {
@@ -8450,7 +8942,10 @@ window.__ModuleLoader__.load({
8450
8942
  // inside the curtain the SHEET is the glass (it has no opacity of its own,
8451
8943
  // so its backdrop-filter samples the page): no second twin here.
8452
8944
  // The grip is the curtain's own bottom edge: drag it to set the height.
8453
- children: sheetOpen ? [panelEl, sheetGripEl] : [panelShadowEl, panelEl]
8945
+ // the docked panel is not BUILT at all when the reader chose the curtain only:
8946
+ // a parked, clipped box still costs a layer and a shadow twin, and there is no
8947
+ // way to open it once its edge pill is gone
8948
+ children: sheetOpen ? [panelEl, sheetGripEl] : (panelMode ? [panelShadowEl, panelEl] : [])
8454
8949
  })]
8455
8950
  });
8456
8951
 
@@ -9681,6 +10176,16 @@ window.__ModuleLoader__.load({
9681
10176
  field("dock", T("settings.dock"), "",
9682
10177
  seg([["left", T("settings.dock.left")], ["right", T("settings.dock.right")]], prefs.dock,
9683
10178
  function (v) { commit({ dock: v }); }), true),
10179
+ // one joined frame, three options: the reader chooses which views EXIST, not
10180
+ // which one happens to be in front
10181
+ field("surface", T("settings.surface"), "",
10182
+ seg([["both", T("settings.surface.both")], ["panel", T("settings.surface.panel")], ["curtain", T("settings.surface.curtain")]],
10183
+ prefs.surface || "both", function (v) { commit({ surface: v }); }), true),
10184
+ // how the scroll position is drawn: a short thumb, or a band that also shows
10185
+ // how much of the list is on screen. The ticks are there either way.
10186
+ field("scrollbar", T("settings.scrollbar"), "",
10187
+ seg([["bar", T("settings.scrollbar.bar")], ["band", T("settings.scrollbar.band")]],
10188
+ prefs.scrollbar || "bar", function (v) { commit({ scrollbar: v }); }), true),
9684
10189
  field("levels", T("settings.levels"), "", levelsControl(true), true),
9685
10190
  field("density", T("settings.density"), "",
9686
10191
  seg([["comfy", T("settings.density.comfy")], ["compact", T("settings.density.compact")]], prefs.density,
@@ -9689,7 +10194,13 @@ window.__ModuleLoader__.load({
9689
10194
  accentDots(prefs.accent, function (v) { commit({ accent: v }); }), true),
9690
10195
  field("glass", T("settings.glass"),
9691
10196
  T("settings.glass.tip") + (autoGlassOff ? " " + T("settings.glass.autoOff") : ""),
9692
- onoff(prefs.glass, function (v) { commit({ glass: v }); }), true),
10197
+ // Turning the material ON is also the reader's answer to an automatic
10198
+ // verdict: the tip already promises "you can turn it back on", so honour it
10199
+ // (the flag lives in localStorage, and the next render reads it again).
10200
+ onoff(prefs.glass, function (v) {
10201
+ if (v) clearAutoGlassOff();
10202
+ commit({ glass: v });
10203
+ }), true),
9693
10204
  field("glassStrength", T("settings.glassStrength"), T("settings.glassStrength.tip"), glassStrengthControl, false),
9694
10205
  field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
9695
10206
  field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
@@ -9697,7 +10208,6 @@ window.__ModuleLoader__.load({
9697
10208
  field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
9698
10209
  field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
9699
10210
  field("remember", T("settings.remember"), "", onoff(prefs.remember, function (v) { commit({ remember: v }); }), true),
9700
- field("autoLoad", T("settings.autoLoad"), "", onoff(prefs.autoLoad, function (v) { commit({ autoLoad: v }); }), true),
9701
10211
  field("debug", T("settings.debug"), T("settings.debug.tip"), onoff(prefs.debug, function (v) { commit({ debug: v }); }), true, true)
9702
10212
  ];
9703
10213
  return react_jsx_runtime.jsxs("li", {
package/lib/index.js CHANGED
@@ -41,6 +41,11 @@ export const Config = z.object({
41
41
  .pattern(/^(left|right)$/)
42
42
  .default("left")
43
43
  .description("Which edge the panel docks to by default")),
44
+ surface: volatileSchema(z
45
+ .string()
46
+ .pattern(/^(both|panel|curtain)$/)
47
+ .default("both")
48
+ .description("Which views the plugin offers: both keeps the docked panel and the curtain, panel removes the curtain, curtain removes the docked panel")),
44
49
  levels: volatileSchema(z
45
50
  .array(z.number().min(1).max(6))
46
51
  .default([1, 2, 3, 4, 5, 6])
@@ -71,6 +76,11 @@ export const Config = z.object({
71
76
  .max(1)
72
77
  .default(1)
73
78
  .description("Height of the curtain as a fraction of the conversation area; drag its bottom edge to change it")),
79
+ scrollbar: volatileSchema(z
80
+ .string()
81
+ .pattern(/^(bar|band)$/)
82
+ .default("bar")
83
+ .description("How the outline draws its scroll position on the right edge: bar is a short vertical thumb, band marks how much of the list is visible")),
74
84
  zoom: volatileSchema(z
75
85
  .number()
76
86
  .min(0.5)
@@ -101,10 +111,7 @@ export const Config = z.object({
101
111
  .boolean()
102
112
  .default(true)
103
113
  .description("Remember where you were reading in each session and return there when it is reopened")),
104
- autoLoad: volatileSchema(z
105
- .boolean()
106
- .default(true)
107
- .description("Load history automatically when the outline is scrolled into turns that are not loaded yet; off means nothing loads until a row is clicked")),
114
+
108
115
  debug: volatileSchema(z
109
116
  .boolean()
110
117
  .default(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, heading-level preferences, a full-width curtain, jump-to-end buttons, title/prompts/full-text/cross-session search and a Chinese/English interface.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "engines": {
20
- "dsh": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0"
20
+ "dsh": ">=0.2.1-rc.2 <0.3.0-0"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "node test/render.mjs"
@@ -42,22 +42,10 @@
42
42
  },
43
43
  "compatibility": {
44
44
  "dshReleases": {
45
- "0.1.5-rc.3": "compatible",
46
- "0.1.7-alpha.1": "compatible",
47
- "0.1.7-alpha.2": "compatible",
48
- "0.1.7-rc.1": "compatible",
49
- "0.1.7-rc.2": "compatible",
50
- "0.2.0-rc.1": "compatible",
51
- "0.2.0-rc.2": "compatible",
52
- "0.2.1-alpha.1": "compatible",
53
- "0.2.1-alpha.2": "compatible",
54
- "0.2.1-alpha.3": "compatible",
55
- "0.2.1-alpha.4": "compatible",
56
- "0.2.1-alpha.5": "compatible",
57
- "0.2.1-rc.1": "compatible",
58
45
  "0.2.1-rc.2": "compatible",
59
46
  "0.2.1-rc.3": "compatible",
60
47
  "0.2.1-rc.4": "compatible",
48
+ "0.2.1-rc.5": "compatible",
61
49
  "0.2.2-alpha.2": "compatible"
62
50
  }
63
51
  }
@@ -65,8 +53,8 @@
65
53
  "peerDependencies": {
66
54
  "react": "^18.2.0",
67
55
  "@deepseek-ai/schemastery": "^3.18.0",
68
- "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0",
69
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.3.0-0"
56
+ "@deepseek-ai/dsh-client-ui-chat": ">=0.2.1-rc.2 <0.3.0-0",
57
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.2 <0.3.0-0"
70
58
  },
71
59
  "peerDependenciesMeta": {
72
60
  "@deepseek-ai/schemastery": {