dsh-auto-collapse 0.1.6 → 0.1.8

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/lib/client.js CHANGED
@@ -36,6 +36,7 @@ var __dshcfBundle = (() => {
36
36
  // src/fold.ts
37
37
  var STYLE_ID = "dshcf-style";
38
38
  var DEFAULT_STATUS_TEXT = "Deep sleeping...";
39
+ var TURN_STATUS_COPY_RE = /Deep diving[.…]*|深度求索中\s*[.…]*/;
39
40
  var ANIM_DURATION_MS = 180;
40
41
  var ANIM_EASING = "ease-out";
41
42
  var DISPLAY_OWNED_PROP = "--dshcf-display-owned";
@@ -58,6 +59,8 @@ var __dshcfBundle = (() => {
58
59
  cordis_stop: "Stop",
59
60
  cordis_undefine: "Remove"
60
61
  };
62
+ var TURN_PROCESS_KIND = "turn-process";
63
+ var NATIVE_OPEN_ATTR = "data-open";
61
64
  var CHIP_CSS = `
62
65
  .dshcf-chip {
63
66
  box-sizing: border-box;
@@ -395,6 +398,7 @@ var __dshcfBundle = (() => {
395
398
  this.bodyTextCache = /* @__PURE__ */ new WeakMap();
396
399
  /** 自上次 pass 以来子树发生变化的 flow 顶层消息;pass 开头统一失效。 */
397
400
  this.dirtyMessages = /* @__PURE__ */ new Set();
401
+ /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
398
402
  /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
399
403
  * 用 Map 不用 WeakMap——switchFlow/stop 需要遍历全量 cancel。 */
400
404
  this.pendingAnims = /* @__PURE__ */ new Map();
@@ -413,6 +417,20 @@ var __dshcfBundle = (() => {
413
417
  * attributes 批次(流式文本、data-state 翻转)不改变块结构,跳过全量
414
418
  * querySelectorAll 重扫(issue #14:长会话下每轮重扫造成主线程卡顿)。 */
415
419
  this.structureDirty = true;
420
+ /** 本轮 pass 的原生 turn 摘要快照(turn id → 开合态):每轮重建,不跨轮复用;
421
+ * data-open 翻转不改变块结构也能驱动重算,见 buildNativeTurnMap。 */
422
+ this.nativeTurns = /* @__PURE__ */ new Map();
423
+ /** 被折叠掏空后藏起的中间包装层(真机 44px vs 28px 真因:think 行全隐后
424
+ * 其父容器变零高度空壳,仍作为 flex item 参与父级 gap,凭空多出 16px)。
425
+ * 内容恢复(展开/流式追加/块转世)时同函数恢复显示;switchFlow 清空。 */
426
+ this.emptiedWrappers = /* @__PURE__ */ new Set();
427
+ /** 本轮 pass 内有过 display 实写(hide/restore 瞬时路径)。空洞发现的触发
428
+ * 条件之一:纯 display 收放不产生 childList,不触发 structureDirty。
429
+ * settle 触发的后续 pass 则由 settleFired 覆盖。读后即清(见发现调用点)。 */
430
+ this.displayTouched = false;
431
+ /** 有 fade 自然结算过(chipSettle 跑过)。结算本身不写 display,但它意味着
432
+ * 某行刚变隐藏——空洞可能刚形成。 */
433
+ this.settleFired = false;
416
434
  /** 滚动稳定化(issue #14):flow 最近的滚动容器缓存(按 flow 身份失效)。 */
417
435
  this.scrollContainer = null;
418
436
  this.scrollContainerFlow = null;
@@ -448,7 +466,10 @@ var __dshcfBundle = (() => {
448
466
  childList: true,
449
467
  subtree: true,
450
468
  attributes: true,
451
- attributeFilter: ["data-selected", "data-state"],
469
+ // 原生 TurnProcess 开合只改 attribute(成员 data-turn-process-hidden、
470
+ // 摘要按钮 data-open),必须进过滤器才能即时调度;插件自身从不写
471
+ // 这两个属性,不会自激。
472
+ attributeFilter: ["data-selected", "data-state", "data-turn-process-hidden", "data-open"],
452
473
  // 流式文本更新(React 改 text node 的 data)属于 characterData
453
474
  // mutation:不观察则二级摘要/滚动跟随只能靠偶发结构变化驱动,
454
475
  // 变成“隔几秒跳一次”。所有文本写入都有守卫(值不变不写),
@@ -518,6 +539,17 @@ var __dshcfBundle = (() => {
518
539
  return;
519
540
  }
520
541
  }
542
+ for (const el of [...this.emptiedWrappers]) {
543
+ if (!el.isConnected) {
544
+ this.emptiedWrappers.delete(el);
545
+ continue;
546
+ }
547
+ if (!this.isHollow(el)) {
548
+ this.restoreElement(el);
549
+ if (el.style.display === "none") el.style.display = "";
550
+ this.emptiedWrappers.delete(el);
551
+ }
552
+ }
521
553
  }
522
554
  stop() {
523
555
  this.disposed = true;
@@ -652,6 +684,7 @@ var __dshcfBundle = (() => {
652
684
  this.currentBlocks = new Map(blocks.map((block) => [block.key, block]));
653
685
  const segments = buildSegments(flow, blocks, (el) => this.hasBodyCached(el));
654
686
  const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
687
+ this.nativeTurns = buildNativeTurnMap(flow);
655
688
  const scrollAnchor = this.captureScrollAnchor(flow);
656
689
  for (const segment of segments) {
657
690
  if (!segment.running) continue;
@@ -668,6 +701,12 @@ var __dshcfBundle = (() => {
668
701
  if (!snapshot.closed || snapshot.running || !snapshot.hasWork) continue;
669
702
  completedKeys.add(snapshot.key);
670
703
  this.completedOnce.add(snapshot.key);
704
+ if (segmentHasNativeTurn(snapshot, this.nativeTurns)) {
705
+ const prev = this.segmentStates.get(snapshot.key);
706
+ prev?.row?.remove();
707
+ if (prev !== void 0) this.segmentStates.delete(snapshot.key);
708
+ continue;
709
+ }
671
710
  let state = this.segmentStates.get(snapshot.key);
672
711
  if (state === void 0) {
673
712
  state = { key: snapshot.key, row: null, expanded: false, snapshot };
@@ -697,6 +736,21 @@ var __dshcfBundle = (() => {
697
736
  seenBlocks.add(block.key);
698
737
  this.reconcileBlock(block, segmentByBlock.get(block.key) ?? null, desiredHidden);
699
738
  }
739
+ if (rebuildBlocks || this.displayTouched || this.settleFired) {
740
+ const hosts = /* @__PURE__ */ new Set();
741
+ for (const block of this.currentBlocks.values()) {
742
+ if (block.host.isConnected) hosts.add(block.host);
743
+ }
744
+ for (const segment of segments) {
745
+ if (segment.finalStep?.isConnected) hosts.add(segment.finalStep);
746
+ for (const middle of segment.middleSteps) {
747
+ if (middle.isConnected) hosts.add(middle);
748
+ }
749
+ }
750
+ this.discoverHollowWrappers(hosts, desiredHidden);
751
+ }
752
+ this.displayTouched = false;
753
+ this.settleFired = false;
700
754
  for (const segment of segments) {
701
755
  const state = this.segmentStates.get(segment.key);
702
756
  const collapse = state !== void 0 && !state.expanded;
@@ -719,6 +773,11 @@ var __dshcfBundle = (() => {
719
773
  if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
720
774
  }
721
775
  this.cleanupStaleChips(seenBlocks);
776
+ for (const [el, record] of [...this.pendingAnims]) {
777
+ if (record.target === "hidden" && record.kind === "fade" && this.pendingAnims.get(el) === record && this.isAnimOverdue(record.anim)) {
778
+ this.finishFadeCollapse(el, record);
779
+ }
780
+ }
722
781
  this.restoreUnusedDisplays(desiredHidden);
723
782
  for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
724
783
  this.stabilizeScrollAfterFold(scrollAnchor);
@@ -808,6 +867,8 @@ var __dshcfBundle = (() => {
808
867
  this.bodyTextCache = /* @__PURE__ */ new WeakMap();
809
868
  this.dirtyMessages.clear();
810
869
  this.auditDisplays.clear();
870
+ this.nativeTurns.clear();
871
+ this.emptiedWrappers.clear();
811
872
  this.structureDirty = true;
812
873
  this.restoreAllDisplays();
813
874
  restoreTurnStatus(this.turnStatusTexts);
@@ -859,7 +920,29 @@ var __dshcfBundle = (() => {
859
920
  }
860
921
  if (row.parentElement !== flow || row.nextElementSibling !== target) target.before(row);
861
922
  }
923
+ /** 原生收起时隐藏本块插件自有 overlay(chip/合并行+内容块):只做
924
+ * display 直写,不清钉住之外的账本、不删展开态,供原生再展开后复用。
925
+ * 在途 WAAPI 动画不追踪取消——收尾回调只做幂等终态对齐,无残留。 */
926
+ hideOverlayForNativeCollapse(block) {
927
+ const existing = this.chips.get(block.key)?.chip;
928
+ if (existing !== void 0 && existing.style.display !== "none") {
929
+ existing.style.marginBottom = "";
930
+ existing.style.display = "none";
931
+ }
932
+ const row = this.mergedThinks.get(block.host);
933
+ if (row !== void 0 && row.style.display !== "none") {
934
+ row.style.display = "none";
935
+ const body = row.nextElementSibling;
936
+ if (body instanceof HTMLElement && body.classList.contains("dshcf-merged-body") && body.style.display !== "none") {
937
+ body.style.display = "none";
938
+ }
939
+ }
940
+ }
862
941
  reconcileBlock(block, segment, desiredHidden) {
942
+ if (blockNativelyCollapsed(block, this.nativeTurns)) {
943
+ this.hideOverlayForNativeCollapse(block);
944
+ return;
945
+ }
863
946
  const state = segment === null ? void 0 : this.segmentStates.get(segment.key);
864
947
  const segmentAnimatableBlocks = segment === null ? void 0 : this.animatableSegmentBlocks.get(segment.key);
865
948
  const animate = this.animatableKeys.has(block.key) || segment !== null && this.animatableKeys.has(segment.key) && (segmentAnimatableBlocks === void 0 || segmentAnimatableBlocks.has(block.key));
@@ -881,7 +964,7 @@ var __dshcfBundle = (() => {
881
964
  existing.style.display = "none";
882
965
  }
883
966
  }
884
- this.releaseMergedThink(block.host, animate);
967
+ this.syncEmptiedWrappers(block, desiredHidden);
885
968
  return;
886
969
  }
887
970
  let expanded = this.blockExpanded.get(block.key) ?? false;
@@ -901,6 +984,7 @@ var __dshcfBundle = (() => {
901
984
  if (chipWasHidden && animate && !(hostAnimate && block.mount === "inside")) this.revealVisual(chip);
902
985
  if (expanded || !this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
903
986
  const chipSettle = () => {
987
+ this.settleFired = true;
904
988
  if (!this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
905
989
  };
906
990
  for (const container of block.containers) {
@@ -922,9 +1006,123 @@ var __dshcfBundle = (() => {
922
1006
  } else {
923
1007
  if (this.releaseMergedThink(block.host, animate, chipSettle)) this.pinChipMargin(chip);
924
1008
  }
1009
+ this.syncEmptiedWrappers(block, desiredHidden);
925
1010
  chip.classList.toggle("dshcf-has-body", block.mount === "inside" && this.hasBodyCached(block.host));
926
1011
  updateChip(chip, block.rows, expanded);
927
1012
  }
1013
+ /**
1014
+ * 掏空包装层对账(真机 44px vs 28px 真因修复)。
1015
+ *
1016
+ * 折叠把某容器的子行全部 display:none 后,该容器变零高度空壳,但仍是
1017
+ * 父级 flex 的 item 并参与 gap(如正文体 flex-column gap:16px),凭空多出
1018
+ * 一份间距。隐藏这类空壳可让 gap 塌缩;子内容恢复可见(展开/流式追加/
1019
+ * 块转世)时恢复显示。
1020
+ *
1021
+ * 安全边界:
1022
+ * - 只看 block.host 内部、不含 host 本人(宿主显隐归 segment 逻辑);
1023
+ * - 跳过结构 seat(data-chat-flow-kind / data-chat-anchor-key)与插件
1024
+ * 自身 overlay(dshcf- 前缀类);
1025
+ * - 只隐藏内容空洞(无可见子内容):零高度无文本的空壳 display:none 与
1026
+ * 保持显示像素一致,只塌缩父级 gap;任何一方恢复内容即恢复显示。
1027
+ */
1028
+ /**
1029
+ * 空洞包装层兜底发现:按宿主直扫 div(仅结构变化轮次调用)。
1030
+ * 只做隐藏侧:空洞即藏并记入 tracked;恢复侧由 syncEmptiedWrappers 的
1031
+ * tracked 复核与 audit 承担。与按行 walk 共用同一安全边界(结构 seat /
1032
+ * 插件 overlay / 宿主本人不碰)。
1033
+ */
1034
+ discoverHollowWrappers(hosts, desiredHidden) {
1035
+ for (const host of hosts) {
1036
+ if (!host.isConnected) continue;
1037
+ let divs;
1038
+ try {
1039
+ divs = [...host.querySelectorAll("div")];
1040
+ } catch {
1041
+ continue;
1042
+ }
1043
+ for (const el of divs) {
1044
+ if (el === host || !(el instanceof HTMLElement)) continue;
1045
+ const cls = el.className;
1046
+ if (typeof cls === "string" && cls.split(" ").some((c) => c.startsWith("dshcf-"))) continue;
1047
+ if (el.hasAttribute("data-chat-flow-kind") || el.hasAttribute("data-chat-anchor-key")) continue;
1048
+ if (this.isHollow(el)) {
1049
+ this.hideElement(el, desiredHidden, false);
1050
+ this.emptiedWrappers.add(el);
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ syncEmptiedWrappers(block, desiredHidden) {
1056
+ const flow = this.flow;
1057
+ if (flow === null) return;
1058
+ const scopeOf = (el) => {
1059
+ let cur = el?.parentElement ?? null;
1060
+ while (cur instanceof HTMLElement && cur.parentElement !== flow) {
1061
+ cur = cur.parentElement;
1062
+ }
1063
+ return cur instanceof HTMLElement && cur.parentElement === flow ? cur : null;
1064
+ };
1065
+ const candidates = [];
1066
+ const seen = /* @__PURE__ */ new Set();
1067
+ const scopes = /* @__PURE__ */ new Set();
1068
+ const collect = (el) => {
1069
+ const scope = scopeOf(el);
1070
+ if (scope === null) return;
1071
+ scopes.add(scope);
1072
+ let node = el?.parentElement ?? null;
1073
+ while (node instanceof HTMLElement && node !== scope) {
1074
+ if (!seen.has(node)) {
1075
+ seen.add(node);
1076
+ candidates.push(node);
1077
+ }
1078
+ node = node.parentElement;
1079
+ }
1080
+ };
1081
+ for (const row of block.rows) collect(row);
1082
+ for (const container of block.containers) collect(container);
1083
+ for (const el of [...this.emptiedWrappers]) {
1084
+ if (!el.isConnected) {
1085
+ this.emptiedWrappers.delete(el);
1086
+ continue;
1087
+ }
1088
+ const scope = scopeOf(el);
1089
+ if (scope === null || !scopes.has(scope)) continue;
1090
+ if (!seen.has(el)) {
1091
+ seen.add(el);
1092
+ candidates.push(el);
1093
+ }
1094
+ }
1095
+ for (const el of candidates) {
1096
+ if (!el.isConnected) {
1097
+ this.emptiedWrappers.delete(el);
1098
+ continue;
1099
+ }
1100
+ const cls = el.className;
1101
+ if (typeof cls === "string" && cls.split(" ").some((c) => c.startsWith("dshcf-"))) continue;
1102
+ if (el.hasAttribute("data-chat-flow-kind") || el.hasAttribute("data-chat-anchor-key")) continue;
1103
+ if (this.isHollow(el)) {
1104
+ this.hideElement(el, desiredHidden, false);
1105
+ this.emptiedWrappers.add(el);
1106
+ } else if (this.emptiedWrappers.delete(el)) {
1107
+ this.restoreElement(el);
1108
+ }
1109
+ }
1110
+ }
1111
+ /** 内容空洞判定:无可见子内容(文本/元素任一可见即非空洞)。隐藏者是谁
1112
+ * 不重要——零高度无文本节点 display:none 与保持显示像素一致(只塌缩父级
1113
+ * gap,正是要修的幻影);任何一方恢复内容,下轮 pass/audit 即恢复显示。
1114
+ * isDisplayed 走 getComputedStyle(桩内退化为内联,语义一致)。 */
1115
+ isHollow(el) {
1116
+ for (const child of [...el.childNodes]) {
1117
+ if (child.nodeType === 3) {
1118
+ if ((child.textContent ?? "").trim() !== "") return false;
1119
+ continue;
1120
+ }
1121
+ if (!(child instanceof HTMLElement)) continue;
1122
+ if (isDisplayed(child)) return false;
1123
+ }
1124
+ return true;
1125
+ }
928
1126
  ensureChip(block) {
929
1127
  let record = this.chips.get(block.key);
930
1128
  const validParent = record !== void 0 && (block.mount === "inside" ? record.chip.parentElement === block.host : record.chip.parentElement === block.host.parentElement);
@@ -1287,6 +1485,7 @@ var __dshcfBundle = (() => {
1287
1485
  }
1288
1486
  el.style.display = "none";
1289
1487
  this.writtenDisplay.set(el, "none");
1488
+ this.displayTouched = true;
1290
1489
  return false;
1291
1490
  }
1292
1491
  restoreElement(el, animate = false) {
@@ -1302,11 +1501,17 @@ var __dshcfBundle = (() => {
1302
1501
  }
1303
1502
  const original = this.originalDisplay.get(el);
1304
1503
  if (!animate || !this.canAnimate(el) || this.hasAnimatingAncestor(el)) {
1305
- if (el.style.display !== original) el.style.display = original;
1504
+ if (el.style.display !== original) {
1505
+ el.style.display = original;
1506
+ this.displayTouched = true;
1507
+ }
1306
1508
  this.releaseDisplayLedger(el);
1307
1509
  return;
1308
1510
  }
1309
- if (el.style.display !== original) el.style.display = original;
1511
+ if (el.style.display !== original) {
1512
+ el.style.display = original;
1513
+ this.displayTouched = true;
1514
+ }
1310
1515
  this.writtenDisplay.set(el, original);
1311
1516
  this.startReveal(el);
1312
1517
  }
@@ -1378,28 +1583,48 @@ var __dshcfBundle = (() => {
1378
1583
  ],
1379
1584
  { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1380
1585
  );
1381
- const record = { anim, target: "hidden", kind: "fade" };
1586
+ const record = { anim, target: "hidden", kind: "fade", settle };
1382
1587
  this.pendingAnims.set(el, record);
1383
- anim.onfinish = () => {
1384
- if (this.pendingAnims.get(el) !== record) return;
1385
- this.pendingAnims.delete(el);
1386
- if (this.displayForeign(el)) {
1387
- this.releaseDisplayLedger(el);
1388
- this.schedule();
1389
- anim.cancel();
1390
- return;
1391
- }
1392
- if (el.style.display !== "none") el.style.display = "none";
1393
- this.writtenDisplay.set(el, "none");
1394
- settle?.();
1395
- anim.cancel();
1396
- this.schedule();
1397
- };
1588
+ anim.onfinish = () => this.finishFadeCollapse(el, record);
1398
1589
  anim.oncancel = () => {
1399
1590
  if (this.pendingAnims.get(el) !== record) return;
1400
1591
  this.pendingAnims.delete(el);
1401
1592
  };
1402
1593
  }
1594
+ /**
1595
+ * 收起 fade 终态结算(onfinish 与僵尸收割共用)。仅被同元素新动画
1596
+ * supersede 时早退;被 sweep 删账(元素已断连)或收割时仍执行 settle——
1597
+ * 回调经 controller 可达,动画必然触发 onfinish,早退会把 chip 内联 16px
1598
+ * 钉住永久残留。重挂载同节点不可能(React 只建新节点),重启动的新动画
1599
+ * 由第一条守卫覆盖,无误伤。
1600
+ */
1601
+ finishFadeCollapse(el, record) {
1602
+ const cur = this.pendingAnims.get(el);
1603
+ if (cur !== void 0 && cur !== record) return;
1604
+ this.pendingAnims.delete(el);
1605
+ if (this.displayForeign(el)) {
1606
+ this.releaseDisplayLedger(el);
1607
+ this.schedule();
1608
+ record.anim.cancel();
1609
+ return;
1610
+ }
1611
+ if (el.style.display !== "none") el.style.display = "none";
1612
+ this.writtenDisplay.set(el, "none");
1613
+ record.settle?.();
1614
+ record.anim.cancel();
1615
+ this.schedule();
1616
+ }
1617
+ /** 动画是否已播完(终态未结算):finish 事件丢失时的兜底判定。WAAPI 桩
1618
+ * 无 playState/effect 时一律 false,走正常事件路径,测试行为不变。 */
1619
+ isAnimOverdue(anim) {
1620
+ const a = anim;
1621
+ if (a.playState === "finished") return true;
1622
+ if (typeof a.currentTime === "number") {
1623
+ const end = a.effect?.getComputedTiming?.().endTime;
1624
+ if (typeof end === "number" && a.currentTime >= end) return true;
1625
+ }
1626
+ return false;
1627
+ }
1403
1628
  /** 轻量视觉 reveal(opacity + 4px 微位移):用于插件全资元素的即时显示
1404
1629
  * 路径——chip(一级展开时出现)与 merged-think 行(二级展开时出现)。
1405
1630
  * 这些元素的 display 完全由插件直写、无 React 协调竞争,因此不入
@@ -1417,7 +1642,12 @@ var __dshcfBundle = (() => {
1417
1642
  }
1418
1643
  restoreUnusedDisplays(desired) {
1419
1644
  for (const el of [...this.controlledDisplay]) {
1420
- if (!desired.has(el)) this.restoreElement(el);
1645
+ if (desired.has(el)) continue;
1646
+ if (!el.isConnected && this.pendingAnims.get(el)?.target === "hidden") {
1647
+ this.releaseDisplayLedger(el);
1648
+ continue;
1649
+ }
1650
+ this.restoreElement(el);
1421
1651
  }
1422
1652
  }
1423
1653
  /** 收集 audit 需要观察的 display 集合;只读取内联样式,不触发布局计算。 */
@@ -1663,6 +1893,39 @@ var __dshcfBundle = (() => {
1663
1893
  if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
1664
1894
  return el.style.display !== "none";
1665
1895
  }
1896
+ function buildNativeTurnMap(flow) {
1897
+ const states = /* @__PURE__ */ new Map();
1898
+ for (const el of flow.children) {
1899
+ if (!(el instanceof HTMLElement)) continue;
1900
+ if (el.getAttribute("data-chat-flow-kind") !== TURN_PROCESS_KIND) continue;
1901
+ const turn = el.getAttribute("data-chat-turn");
1902
+ if (turn === null || turn === "") continue;
1903
+ const button = el.querySelector("button[data-turn-process]");
1904
+ if (button === null) continue;
1905
+ const openAttr = button.getAttribute(NATIVE_OPEN_ATTR) !== null;
1906
+ const expandedAttr = button.getAttribute("aria-expanded");
1907
+ const open = expandedAttr === null ? openAttr : expandedAttr === "true";
1908
+ states.set(turn, { collapsed: !open });
1909
+ }
1910
+ return states;
1911
+ }
1912
+ function elementTurn(el) {
1913
+ const turn = el.getAttribute("data-chat-turn");
1914
+ return turn === null || turn === "" ? null : turn;
1915
+ }
1916
+ function segmentHasNativeTurn(segment, nativeTurns) {
1917
+ if (nativeTurns.size === 0) return false;
1918
+ for (const block of segment.blocks) {
1919
+ const turn = elementTurn(block.host);
1920
+ if (turn !== null && nativeTurns.has(turn)) return true;
1921
+ }
1922
+ return false;
1923
+ }
1924
+ function blockNativelyCollapsed(block, nativeTurns) {
1925
+ if (nativeTurns.size === 0) return false;
1926
+ const turn = elementTurn(block.host);
1927
+ return turn !== null && nativeTurns.get(turn)?.collapsed === true;
1928
+ }
1666
1929
  function stableElementKey(el, fallbackIndex) {
1667
1930
  const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
1668
1931
  const key = el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`;
@@ -1776,6 +2039,7 @@ var __dshcfBundle = (() => {
1776
2039
  };
1777
2040
  for (const el of children) {
1778
2041
  const kind = el.getAttribute("data-chat-flow-kind");
2042
+ if (kind === TURN_PROCESS_KIND) continue;
1779
2043
  if (kind === "user" || kind === "steering" || kind === "turn-tail") {
1780
2044
  flushCarry();
1781
2045
  run = null;
@@ -2127,14 +2391,23 @@ var __dshcfBundle = (() => {
2127
2391
  const statuses = flow.matches('[role="status"]') ? [flow, ...flow.querySelectorAll('[role="status"]')] : [...flow.querySelectorAll('[role="status"]')];
2128
2392
  for (const status of statuses) {
2129
2393
  for (const node of status.childNodes) {
2130
- if (node instanceof Text && node.data.includes("Deep diving")) {
2131
- let record = originals.get(node);
2394
+ if (!(node instanceof Text)) continue;
2395
+ let record = originals.get(node);
2396
+ if (record !== void 0 && node.data === record.written) {
2397
+ const next = record.original.replace(TURN_STATUS_COPY_RE, statusText);
2398
+ if (node.data !== next) {
2399
+ node.data = next;
2400
+ record.written = next;
2401
+ }
2402
+ continue;
2403
+ }
2404
+ if (TURN_STATUS_COPY_RE.test(node.data)) {
2132
2405
  if (record === void 0) {
2133
2406
  record = { original: node.data, written: "" };
2134
2407
  originals.set(node, record);
2135
2408
  }
2136
2409
  if (node.data !== record.written) record.original = node.data;
2137
- const next = node.data.replace(/Deep diving[.…]*/, statusText);
2410
+ const next = node.data.replace(TURN_STATUS_COPY_RE, statusText);
2138
2411
  if (node.data !== next) {
2139
2412
  node.data = next;
2140
2413
  record.written = next;
@@ -2257,6 +2530,7 @@ var __dshcfBundle = (() => {
2257
2530
  .dshcf-settings-save:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
2258
2531
  `;
2259
2532
  var STYLE_ID2 = "dshcf-settings-style";
2533
+ var styleOwners = 0;
2260
2534
  function injectCardStyle() {
2261
2535
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2) !== null) return;
2262
2536
  const style = document.createElement("style");
@@ -2264,6 +2538,17 @@ var __dshcfBundle = (() => {
2264
2538
  style.textContent = CARD_CSS;
2265
2539
  document.head.appendChild(style);
2266
2540
  }
2541
+ function acquireCardStyle() {
2542
+ styleOwners += 1;
2543
+ injectCardStyle();
2544
+ let released = false;
2545
+ return () => {
2546
+ if (released) return;
2547
+ released = true;
2548
+ styleOwners = Math.max(0, styleOwners - 1);
2549
+ if (styleOwners === 0 && typeof document !== "undefined") document.getElementById(STYLE_ID2)?.remove();
2550
+ };
2551
+ }
2267
2552
  function ChevronIcon(open) {
2268
2553
  const React = __require("react");
2269
2554
  const className = open ? "dshcf-settings-chevron dshcf-settings-chevronOpen" : "dshcf-settings-chevron";
@@ -2337,7 +2622,7 @@ var __dshcfBundle = (() => {
2337
2622
  [
2338
2623
  React.createElement("span", { className: "dshcf-settings-headText" }, [
2339
2624
  React.createElement("span", { className: "dshcf-settings-name" }, "\u72B6\u6001\u63D0\u793A\u8BCD"),
2340
- React.createElement("span", { className: "dshcf-settings-description" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD\uFF0C\u53EF\u4EE5\u66FF\u6362\u539F\u6709\u7684Deep diving...\u4E00\u884C\uFF0C\u7531\u63D2\u4EF6dsh-auto-collapse\u63D0\u4F9B")
2625
+ React.createElement("span", { className: "dshcf-settings-description" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD\uFF0C\u53EF\u4EE5\u66FF\u6362\u539F\u6709\u7684\u8FD0\u884C\u72B6\u6001\u6587\u6848\uFF08\u6DF1\u5EA6\u6C42\u7D22\u4E2D...\uFF09\uFF0C\u7531\u63D2\u4EF6dsh-auto-collapse\u63D0\u4F9B")
2341
2626
  ]),
2342
2627
  dirty ? React.createElement("span", { className: "dshcf-settings-pending" }, "\u672A\u4FDD\u5B58") : null,
2343
2628
  ChevronIcon(open)
@@ -2358,11 +2643,11 @@ var __dshcfBundle = (() => {
2358
2643
  className: "dshcf-settings-input",
2359
2644
  type: "text",
2360
2645
  value: text,
2361
- placeholder: "Deep diving...",
2646
+ placeholder: "\u6DF1\u5EA6\u6C42\u7D22\u4E2D...",
2362
2647
  disabled: !writable,
2363
2648
  onChange: (event) => edit(event.target.value)
2364
2649
  }),
2365
- React.createElement("p", { className: "dshcf-settings-hint" }, "\u4E3A\u7A7A\u65F6\u6062\u590D\u9ED8\u8BA4Deep diving...\u63D0\u793A\u8BCD\u72B6\u6001")
2650
+ React.createElement("p", { className: "dshcf-settings-hint" }, "\u4E3A\u7A7A\u65F6\u6062\u590D\u5B98\u65B9\u9ED8\u8BA4\u6587\u6848\uFF08\u6DF1\u5EA6\u6C42\u7D22\u4E2D...\uFF09")
2366
2651
  ]),
2367
2652
  React.createElement("div", { className: "dshcf-settings-footer" }, [
2368
2653
  failed ? React.createElement("p", { className: "dshcf-settings-failed", role: "status" }, "\u672C\u90E8\u7F72\u6CA1\u6709\u63A5\u53D7\u8FD9\u4E9B\u503C\uFF0C\u5DF2\u4FDD\u7559\u4F9B\u4F60\u4FEE\u6539\u3002") : null,
@@ -2373,35 +2658,67 @@ var __dshcfBundle = (() => {
2373
2658
  ]);
2374
2659
  }
2375
2660
  function setupSettingsCard(ctx, scope) {
2376
- injectCardStyle();
2377
- return ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
2378
- {
2379
- name: "settings.plugin.item",
2380
- key: AUTO_COLLAPSE_NS,
2381
- inject: () => ({ scope })
2382
- },
2383
- StatusTextCard
2384
- ));
2661
+ const releaseStyle = acquireCardStyle();
2662
+ try {
2663
+ const offSlot = ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
2664
+ {
2665
+ name: "settings.plugin.item",
2666
+ key: AUTO_COLLAPSE_NS,
2667
+ inject: () => ({ scope })
2668
+ },
2669
+ StatusTextCard
2670
+ ));
2671
+ return () => {
2672
+ try {
2673
+ offSlot();
2674
+ } finally {
2675
+ releaseStyle();
2676
+ }
2677
+ };
2678
+ } catch (error) {
2679
+ releaseStyle();
2680
+ throw error;
2681
+ }
2385
2682
  }
2386
2683
 
2387
2684
  // src/client.ts
2388
2685
  var name = "dsh-auto-collapse";
2389
2686
  var inject = [];
2390
2687
  function apply(ctx) {
2688
+ const fallbackStatusText = () => DEFAULT_STATUS_TEXT2;
2689
+ let readStatusText = fallbackStatusText;
2690
+ const controller = new FoldController(() => readStatusText());
2391
2691
  ctx.effect(() => {
2392
- const settingsScope = ctx.get?.("settingsScope");
2393
- const slots = ctx.get?.("slots");
2394
- const scope = settingsScope?.bind({ namespace: AUTO_COLLAPSE_NS });
2395
- const controller = new FoldController(statusTextProvider(scope));
2396
2692
  controller.start();
2397
- const offScope = scope?.subscribe(() => controller.refresh());
2398
- const offSettings = slots === void 0 || scope === void 0 ? void 0 : setupSettingsCard({ slots }, scope);
2399
- return () => {
2400
- offScope?.();
2401
- offSettings?.();
2402
- controller.stop();
2403
- };
2404
- }, "dsh-auto-collapse: fold observer + settings card");
2693
+ return () => controller.stop();
2694
+ }, "dsh-auto-collapse: fold observer");
2695
+ ctx.inject?.(["settingsScope", "slots"], (serviceCtx) => {
2696
+ serviceCtx.effect(() => {
2697
+ const settingsScope = serviceCtx.get?.("settingsScope");
2698
+ const slots = serviceCtx.get?.("slots");
2699
+ if (settingsScope === void 0 || slots === void 0) return;
2700
+ const scope = settingsScope.bind({ namespace: AUTO_COLLAPSE_NS });
2701
+ const scopedStatusText = statusTextProvider(scope);
2702
+ const offScope = scope.subscribe(() => controller.refresh());
2703
+ let offSettings;
2704
+ try {
2705
+ offSettings = setupSettingsCard({ slots }, scope);
2706
+ } catch (error) {
2707
+ offScope();
2708
+ throw error;
2709
+ }
2710
+ readStatusText = scopedStatusText;
2711
+ controller.refresh();
2712
+ return () => {
2713
+ offScope();
2714
+ offSettings();
2715
+ if (readStatusText === scopedStatusText) {
2716
+ readStatusText = fallbackStatusText;
2717
+ controller.refresh();
2718
+ }
2719
+ };
2720
+ }, "dsh-auto-collapse: settings scope + plugin card");
2721
+ });
2405
2722
  }
2406
2723
  return __toCommonJS(client_exports);
2407
2724
  })();