dsh-auto-collapse 0.1.3 → 0.1.5

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
@@ -5,6 +5,12 @@ var __dshcfBundle = (() => {
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
8
14
  var __export = (target, all) => {
9
15
  for (var name2 in all)
10
16
  __defProp(target, name2, { get: all[name2], enumerable: true });
@@ -29,6 +35,12 @@ var __dshcfBundle = (() => {
29
35
 
30
36
  // src/fold.ts
31
37
  var STYLE_ID = "dshcf-style";
38
+ var DEFAULT_STATUS_TEXT = "Deep sleeping...";
39
+ var ANIM_DURATION_MS = 180;
40
+ var ANIM_EASING = "ease-out";
41
+ var DISPLAY_OWNED_PROP = "--dshcf-display-owned";
42
+ var AUDIT_TICK_MS = 1e3;
43
+ var STICK_BOTTOM_THRESHOLD_PX = 24;
32
44
  var TOOL_LABELS = {
33
45
  bash: "Bash",
34
46
  pwsh: "Pwsh",
@@ -68,6 +80,12 @@ var __dshcfBundle = (() => {
68
80
  text-align: left;
69
81
  cursor: pointer;
70
82
  user-select: none;
83
+ /* \u5C55\u5F00\u6001\u8865\u7684 margin-bottom 16px \u7531 aria-expanded/has-body \u7FFB\u8F6C\u9A71\u52A8\uFF0C
84
+ \u4E00\u5E27\u77AC\u5F00\uFF08\u4E0E\u4E09\u7EA7\u884C display \u7FFB\u8F6C\u540C pass \u540C\u5E27\uFF0C\u65E0\u4E0B\u63A8\uFF09\uFF1B\u6536\u8D77\u65B9\u5411
85
+ \u7531 JS \u4FA7\u9489\u4F4F\u95F4\u8DDD\uFF08\u6536\u8D77 fade \u671F\u95F4\u5185\u8054 16px\uFF0C\u6700\u540E\u4E00\u6761\u5728\u9014\u6E10\u9690 settle
86
+ \u540E\u5F52\u96F6\uFF0C\u89C1 reconcileBlock / hasPendingCollapse\uFF09\u3002\u4E0D\u8BBE CSS transition
87
+ \u2014\u2014v13 \u7684\u8FC7\u6E21\u4E0E chip \u5143\u7D20\u751F\u547D\u5468\u671F\u968F\u673A\u4EA4\u4E92\uFF0C\u4EA7\u751F\u5C55\u5F00\u65B9\u5411\u53CC\u91CD\u4EBA\u683C
88
+ \uFF08\u590D\u7528\u5143\u7D20\u7F13\u52A8\u4E0B\u63A8\u4E09\u7EA7\u884C vs \u65B0\u5EFA\u5143\u7D20\u77AC\u5F00\uFF09\uFF0C\u540C\u7C7B\u578B\u5757\u4E0D\u4E00\u81F4\u3002 */
71
89
  }
72
90
  .dshcf-chip[aria-expanded="true"],
73
91
  .dshcf-chip.dshcf-has-body {
@@ -328,8 +346,16 @@ var __dshcfBundle = (() => {
328
346
  }
329
347
  `;
330
348
  var FoldController = class {
331
- constructor() {
349
+ constructor(statusTextProvider2, options) {
332
350
  this.observer = null;
351
+ /** 防止同一个控制器重复注册 observer、定时器和可见性监听。 */
352
+ this.started = false;
353
+ /** body 尚未创建时等待 DOMContentLoaded,再补一次启动。 */
354
+ this.waitingForBody = false;
355
+ this.onDomContentLoaded = () => {
356
+ this.waitingForBody = false;
357
+ this.start();
358
+ };
333
359
  this.raf = 0;
334
360
  this.timer = 0;
335
361
  this.disposed = false;
@@ -351,18 +377,72 @@ var __dshcfBundle = (() => {
351
377
  this.segmentStates = /* @__PURE__ */ new Map();
352
378
  /** segment 首次观察到 running 的时间,用于没有官方时长的实时回合。 */
353
379
  this.runningSince = /* @__PURE__ */ new Map();
380
+ /** 曾完成过的 segment key:段恢复运行时据此重开本地计时,防止重新结算
381
+ * 的本地时长吞掉完成间隙。 */
382
+ this.completedOnce = /* @__PURE__ */ new Set();
354
383
  /** 插件改写 display 前的精确原值;受控集合用于分类漂移和 stop() 恢复。 */
355
384
  this.originalDisplay = /* @__PURE__ */ new WeakMap();
356
385
  this.controlledDisplay = /* @__PURE__ */ new Set();
357
- /** 被改写为 Deep sleeping 的原生状态文本,卸载时按节点恢复。 */
386
+ /** 元素 → 插件最后确保的 display 值:恢复前与当前内联值比对,漂移即视为
387
+ * 外部介入(镜像 turnStatusTexts 的 original/written 双快照守卫,issue #11)。 */
388
+ this.writtenDisplay = /* @__PURE__ */ new WeakMap();
389
+ /** 被改写为状态提示词的原生状态文本:original = 宿主原文(卸载还原用),
390
+ * written = 插件最后一次写入的值(仅当节点仍等于它时才还原,避免覆盖
391
+ * 宿主在插件写入之后的状态更新)。 */
358
392
  this.turnStatusTexts = /* @__PURE__ */ new Map();
393
+ /** 正文判定缓存(消息元素 → 有无正文):流式期间只有被 mutation 命中的
394
+ * 消息失效重算,历史消息跨 pass 复用,避免每帧全量 TreeWalker。 */
395
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
396
+ /** 自上次 pass 以来子树发生变化的 flow 顶层消息;pass 开头统一失效。 */
397
+ this.dirtyMessages = /* @__PURE__ */ new Set();
398
+ /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
399
+ * 用 Map 不用 WeakMap——switchFlow/stop 需要遍历全量 cancel。 */
400
+ this.pendingAnims = /* @__PURE__ */ new Map();
401
+ /** 手势点击的一次性可动画 block key;segment 级点击另保留中间正文的门控。 */
402
+ this.animatableKeys = /* @__PURE__ */ new Set();
403
+ /** segment 点击时只让点击前已存在的 block 播放 reveal;流式中新出现的
404
+ * 临时分裂块直接显示,避免分类收敛时留下半透明 stale chip。 */
405
+ this.animatableSegmentBlocks = /* @__PURE__ */ new Map();
406
+ /** 外部变更对账定时器句柄(自重排 setTimeout 链,见 armAuditLoop)。 */
407
+ this.auditTimer = 0;
408
+ /** 上一轮 pass 记录的关键元素内联 display,用于 audit 轻量检测漂移。
409
+ * audit 只读这份快照,不在页面稳定时重新执行完整 pass。 */
410
+ this.auditDisplays = /* @__PURE__ */ new Map();
411
+ /** 自上次 pass 以来 flow 子树发生过结构变化(childList)或正文判定翻转:
412
+ * 为 true 时 pass 重建分块快照,否则复用 currentBlocks——characterData/
413
+ * attributes 批次(流式文本、data-state 翻转)不改变块结构,跳过全量
414
+ * querySelectorAll 重扫(issue #14:长会话下每轮重扫造成主线程卡顿)。 */
415
+ this.structureDirty = true;
416
+ /** 滚动稳定化(issue #14):flow 最近的滚动容器缓存(按 flow 身份失效)。 */
417
+ this.scrollContainer = null;
418
+ this.scrollContainerFlow = null;
419
+ /** 回到前台立即补一轮对账;后台 tab 由 document.hidden 门控跳过。 */
420
+ this.onVisibilityChange = () => {
421
+ if (typeof document === "undefined" || document.hidden !== true) this.schedule();
422
+ };
423
+ this.statusTextProvider = statusTextProvider2 ?? (() => DEFAULT_STATUS_TEXT);
424
+ this.auditIntervalMs = options?.auditIntervalMs ?? AUDIT_TICK_MS;
425
+ }
426
+ /** 设置变更后重跑一轮,让状态提示词立即生效。 */
427
+ refresh() {
428
+ this.schedule();
359
429
  }
360
430
  start() {
361
- if (this.disposed) return;
431
+ if (this.disposed || this.started || this.waitingForBody) return;
432
+ if (typeof document === "undefined") return;
433
+ if (document.body === null) {
434
+ if (typeof document.addEventListener !== "function") return;
435
+ this.waitingForBody = true;
436
+ document.addEventListener("DOMContentLoaded", this.onDomContentLoaded, { once: true });
437
+ return;
438
+ }
362
439
  injectStyle();
363
440
  try {
364
441
  this.observer = new MutationObserver((records) => {
365
- if (this.shouldSchedule(records)) this.schedule();
442
+ if (this.shouldSchedule(records)) {
443
+ this.markDirty(records);
444
+ this.schedule();
445
+ }
366
446
  });
367
447
  this.observer.observe(document.body, {
368
448
  childList: true,
@@ -375,16 +455,86 @@ var __dshcfBundle = (() => {
375
455
  // 不会自激。
376
456
  characterData: true
377
457
  });
458
+ this.started = true;
459
+ this.armAuditLoop();
378
460
  this.schedule();
379
461
  } catch (error) {
462
+ this.observer?.disconnect();
463
+ this.observer = null;
464
+ this.started = false;
380
465
  this.reportError(error);
381
466
  throw error;
382
467
  }
383
468
  }
469
+ /** 外部显示变更对账循环(issue #11 Bug B):外部对宿主行的 style 写入不产生
470
+ * observer record(style 不在 attributeFilter 内,监听会因插件自身直写 style
471
+ * 自激),改用低频轻量对账兜底——发现漂移后才由 pass 收敛;
472
+ * 后台 tab 由 document.hidden 门控跳过,回前台由 visibilitychange 立即补一轮。
473
+ * 用自重排 setTimeout 链而非 setInterval:与 schedule 的兜底定时器同源,
474
+ * 测试桩 clearTimers 后链条自然熄灭。 */
475
+ armAuditLoop() {
476
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
477
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
478
+ }
479
+ this.rearmAudit();
480
+ }
481
+ rearmAudit() {
482
+ if (this.disposed || this.auditTimer !== 0) return;
483
+ this.auditTimer = setTimeout(() => {
484
+ this.auditTimer = 0;
485
+ if (this.disposed) return;
486
+ if (typeof document !== "undefined" && document.hidden === true) {
487
+ this.rearmAudit();
488
+ return;
489
+ }
490
+ this.audit();
491
+ this.rearmAudit();
492
+ }, this.auditIntervalMs);
493
+ }
494
+ /** 低成本显示状态对账:只有发现外部漂移时才启动完整 pass。
495
+ *
496
+ * 外部 style.display 写入不会产生当前 observer 的 attribute 记录,
497
+ * 因此仍保留 audit;但稳定页面不应每秒重扫整个 flow。快照只覆盖
498
+ * flow 顶层行、插件控制中的宿主行和插件自有展示行,避免引入布局读取。 */
499
+ audit() {
500
+ if (this.disposed) return;
501
+ const flow = this.flow;
502
+ if (flow === null || !flow.isConnected) {
503
+ if (flow !== null) this.schedule();
504
+ return;
505
+ }
506
+ const current = this.collectAuditDisplays(flow);
507
+ if (current.size !== this.auditDisplays.size) {
508
+ this.schedule();
509
+ return;
510
+ }
511
+ for (const [el, display] of current) {
512
+ if (this.auditDisplays.get(el) !== display) {
513
+ this.schedule();
514
+ return;
515
+ }
516
+ if (this.controlledDisplay.has(el) && this.displayForeign(el)) {
517
+ this.schedule();
518
+ return;
519
+ }
520
+ }
521
+ }
384
522
  stop() {
385
523
  this.disposed = true;
524
+ this.started = false;
525
+ this.waitingForBody = false;
526
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
527
+ document.removeEventListener("DOMContentLoaded", this.onDomContentLoaded);
528
+ }
386
529
  if (this.raf !== 0) cancelAnimationFrame(this.raf);
387
530
  if (this.timer !== 0) clearTimeout(this.timer);
531
+ if (this.auditTimer !== 0) {
532
+ clearTimeout(this.auditTimer);
533
+ this.auditTimer = 0;
534
+ }
535
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
536
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
537
+ }
388
538
  this.observer?.disconnect();
389
539
  this.switchFlow(null);
390
540
  removeStyle();
@@ -392,7 +542,51 @@ var __dshcfBundle = (() => {
392
542
  /** body 级 observer 只负责发现 flow 替换;已有 flow 外的文本变化不再触发全量扫描。 */
393
543
  shouldSchedule(records) {
394
544
  if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
395
- return records.some((record) => nodeWithin(record.target, this.flow) || nodeWithin(this.flow, record.target));
545
+ return records.some((record) => this.isRelevantMutation(record));
546
+ }
547
+ /** 判断 mutation 是否会影响宿主 flow;插件自有节点的回写直接忽略,
548
+ * 避免“pass 插入 chip → observer 再开一轮 pass”的自激循环。 */
549
+ isRelevantMutation(record) {
550
+ const flow = this.flow;
551
+ if (flow === null || !flow.isConnected) return true;
552
+ if (!nodeWithin(record.target, flow) && !nodeWithin(flow, record.target)) return false;
553
+ if (record.type === "characterData" && record.target instanceof Text && this.turnStatusTexts.has(record.target)) {
554
+ return false;
555
+ }
556
+ const changed = [
557
+ ...Array.from(record.addedNodes ?? []),
558
+ ...Array.from(record.removedNodes ?? [])
559
+ ];
560
+ if (changed.length > 0 && changed.every(isPluginOwnedNode)) return false;
561
+ if (changed.length === 0 && isPluginOwnedNode(record.target)) return false;
562
+ return true;
563
+ }
564
+ /** 记录本批 mutation 命中的 flow 顶层消息,供正文判定缓存定向失效。
565
+ * 从 record.target 沿 parentNode 走到 flow 的直接子级即所属消息。
566
+ * 失效粒度(issue #14):只有 childList 使分块快照失效;flow 直挂层的
567
+ * 插件节点/文本节点与 flow 外的混批记录不影响任何消息的正文判定,跳过
568
+ * 而非全量失效——旧逻辑把它们全部放大成 O(全会话) 的 TreeWalker 重扫,
569
+ * 长会话流式期间每帧如此。空批次仍保守全量失效(测试桩的调度通知)。 */
570
+ markDirty(records) {
571
+ const flow = this.flow;
572
+ if (flow === null || !flow.isConnected) return;
573
+ if (records.length === 0) {
574
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
575
+ this.dirtyMessages.clear();
576
+ this.structureDirty = true;
577
+ return;
578
+ }
579
+ for (const record of records.filter((record2) => this.isRelevantMutation(record2))) {
580
+ if (record.type === "childList") this.structureDirty = true;
581
+ if (record.target === flow) {
582
+ for (const node of record.addedNodes ?? []) {
583
+ if (node instanceof HTMLElement) this.dirtyMessages.add(node);
584
+ }
585
+ continue;
586
+ }
587
+ const owner = flowChildOwner(record.target, flow);
588
+ if (owner !== null) this.dirtyMessages.add(owner);
589
+ }
396
590
  }
397
591
  schedule() {
398
592
  if (this.disposed || this.raf !== 0) return;
@@ -424,6 +618,9 @@ var __dshcfBundle = (() => {
424
618
  style?.removeAttribute("data-dshcf-error");
425
619
  } catch (error) {
426
620
  this.reportError(error);
621
+ } finally {
622
+ this.animatableKeys.clear();
623
+ this.animatableSegmentBlocks.clear();
427
624
  }
428
625
  }
429
626
  reportError(error) {
@@ -442,12 +639,27 @@ var __dshcfBundle = (() => {
442
639
  if (nextFlow !== this.flow) this.switchFlow(nextFlow);
443
640
  const flow = this.flow;
444
641
  if (flow === null) return;
445
- const blocks = findBlocks(flow);
642
+ let bodyFlipped = false;
643
+ for (const el of this.dirtyMessages) {
644
+ const prev = this.bodyTextCache.get(el);
645
+ this.bodyTextCache.delete(el);
646
+ if (prev !== void 0 && this.hasBodyCached(el) !== prev) bodyFlipped = true;
647
+ }
648
+ this.dirtyMessages.clear();
649
+ const rebuildBlocks = this.structureDirty || bodyFlipped || this.currentBlocks.size === 0;
650
+ this.structureDirty = false;
651
+ const blocks = rebuildBlocks ? findBlocks(flow, (el) => this.hasBodyCached(el)) : [...this.currentBlocks.values()];
446
652
  this.currentBlocks = new Map(blocks.map((block) => [block.key, block]));
447
- const segments = buildSegments(flow, blocks);
653
+ const segments = buildSegments(flow, blocks, (el) => this.hasBodyCached(el));
448
654
  const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
655
+ const scrollAnchor = this.captureScrollAnchor(flow);
449
656
  for (const segment of segments) {
450
- if (segment.running && !this.runningSince.has(segment.key)) {
657
+ if (!segment.running) continue;
658
+ if (this.completedOnce.has(segment.key)) {
659
+ this.completedOnce.delete(segment.key);
660
+ this.runningSince.delete(segment.key);
661
+ }
662
+ if (!this.runningSince.has(segment.key)) {
451
663
  this.runningSince.set(segment.key, Date.now());
452
664
  }
453
665
  }
@@ -455,6 +667,7 @@ var __dshcfBundle = (() => {
455
667
  for (const snapshot of segments) {
456
668
  if (!snapshot.closed || snapshot.running || !snapshot.hasWork) continue;
457
669
  completedKeys.add(snapshot.key);
670
+ this.completedOnce.add(snapshot.key);
458
671
  let state = this.segmentStates.get(snapshot.key);
459
672
  if (state === void 0) {
460
673
  state = { key: snapshot.key, row: null, expanded: false, snapshot };
@@ -465,7 +678,7 @@ var __dshcfBundle = (() => {
465
678
  const started = this.runningSince.get(snapshot.key);
466
679
  const parsed = snapshot.boundary === null ? void 0 : parseTurnDuration(snapshot.boundary);
467
680
  if (parsed !== void 0) state.duration = parsed;
468
- else if (started !== void 0) state.duration = Date.now() - started;
681
+ else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
469
682
  if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
470
683
  this.syncProcessedRow(state);
471
684
  }
@@ -487,9 +700,10 @@ var __dshcfBundle = (() => {
487
700
  for (const segment of segments) {
488
701
  const state = this.segmentStates.get(segment.key);
489
702
  const collapse = state !== void 0 && !state.expanded;
703
+ const animate = this.animatableKeys.has(segment.key);
490
704
  for (const middle of segment.middleSteps) {
491
- if (collapse) this.hideElement(middle, desiredHidden);
492
- else this.restoreElement(middle);
705
+ if (collapse) this.hideElement(middle, desiredHidden, animate);
706
+ else this.restoreElement(middle, animate);
493
707
  }
494
708
  if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
495
709
  }
@@ -507,17 +721,81 @@ var __dshcfBundle = (() => {
507
721
  this.cleanupStaleChips(seenBlocks);
508
722
  this.restoreUnusedDisplays(desiredHidden);
509
723
  for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
724
+ this.stabilizeScrollAfterFold(scrollAnchor);
510
725
  for (const key of [...this.runningSince.keys()]) {
511
726
  if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
512
727
  }
728
+ for (const key of [...this.completedOnce]) {
729
+ if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
730
+ }
513
731
  for (const [node] of [...this.turnStatusTexts]) {
514
732
  if (!node.isConnected) this.turnStatusTexts.delete(node);
515
733
  }
516
- replaceTurnStatus(flow, this.turnStatusTexts);
734
+ for (const [el] of [...this.pendingAnims]) {
735
+ if (!el.isConnected) this.pendingAnims.delete(el);
736
+ }
737
+ const statusText = this.statusTextProvider();
738
+ if (statusText === void 0 || statusText === "") {
739
+ restoreTurnStatus(this.turnStatusTexts);
740
+ } else {
741
+ replaceTurnStatus(flow, this.turnStatusTexts, statusText);
742
+ }
743
+ this.captureAuditDisplays(flow);
744
+ }
745
+ /** flow 最近的滚动容器(issue #14 滚动稳定化的测量基准):沿父链找第一个
746
+ * overflow-y 为 auto/scroll 且实际可滚动的祖先。结果按 flow 身份缓存;
747
+ * 找不到时不缓存(内容增长后祖先可能变为可滚动),每 pass 重探的代价
748
+ * 只是几次 clean-layout 的 computed style / scrollHeight 读取。 */
749
+ findScrollContainer(flow) {
750
+ if (this.scrollContainerFlow === flow && this.scrollContainer !== null && this.scrollContainer.isConnected) {
751
+ return this.scrollContainer;
752
+ }
753
+ let node = flow.parentElement;
754
+ while (node !== null) {
755
+ if (typeof getComputedStyle === "function") {
756
+ const oy = getComputedStyle(node).overflowY;
757
+ if ((oy === "auto" || oy === "scroll") && node.scrollHeight > node.clientHeight) break;
758
+ }
759
+ node = node.parentElement;
760
+ }
761
+ if (node === null) return null;
762
+ this.scrollContainer = node;
763
+ this.scrollContainerFlow = flow;
764
+ return node;
765
+ }
766
+ /** 几何写入前捕捉贴底意图(issue #14):视口距底 ≤ 上游 FOLLOW_THRESHOLD
767
+ * 时返回锚点,stabilizeScrollAfterFold 在写入后同帧钉回底部。远离底部
768
+ * (用户正在滚动浏览)时不干预——视口上方的高度变化由浏览器 scroll
769
+ * anchoring 补偿,视口下方的折叠不可见,插件再写 scrollTop 只会加入
770
+ * 上游吸底回写的拉锯。 */
771
+ captureScrollAnchor(flow) {
772
+ const scroller = this.findScrollContainer(flow);
773
+ if (scroller === null) return null;
774
+ const dist = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
775
+ return dist <= STICK_BOTTOM_THRESHOLD_PX ? { el: scroller } : null;
776
+ }
777
+ /** 几何写入后把贴底视口钉回底部:折叠让 scrollHeight 缩小时若不在此帧
778
+ * 补写 scrollTop,宿主 ChatView 要到下一帧 ResizeObserver 才吸底回写,
779
+ * 中间的空档让触控板惯性滚动乘虚而入,反复折叠时表现为上下抽搐(用户
780
+ * 实测:长会话滚到底部附近无法稳定定位)。同一帧内钉回后,宿主的吸底
781
+ * 回写成为幂等 no-op,不再是第二个 scrollTop 写入方。 */
782
+ stabilizeScrollAfterFold(anchor) {
783
+ if (anchor === null) return;
784
+ const el = anchor.el;
785
+ if (!el.isConnected) return;
786
+ const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
787
+ if (dist > STICK_BOTTOM_THRESHOLD_PX) el.scrollTop = el.scrollHeight - el.clientHeight;
517
788
  }
518
789
  /** flow 元素变化即视为会话切换:完整恢复旧 flow,再从新 DOM 重建。 */
519
790
  switchFlow(next) {
520
791
  if (next === this.flow) return;
792
+ for (const [el, record] of this.pendingAnims) {
793
+ record.anim.cancel();
794
+ if (record.kind === "height") this.clearCollapseLock(el);
795
+ }
796
+ this.pendingAnims.clear();
797
+ this.animatableKeys.clear();
798
+ this.animatableSegmentBlocks.clear();
521
799
  for (const record of this.chips.values()) record.chip.remove();
522
800
  this.chips.clear();
523
801
  for (const host of [...this.mergedThinks.keys()]) this.removeMergedThink(host);
@@ -526,6 +804,11 @@ var __dshcfBundle = (() => {
526
804
  this.currentBlocks.clear();
527
805
  this.blockExpanded.clear();
528
806
  this.runningSince.clear();
807
+ this.completedOnce.clear();
808
+ this.bodyTextCache = /* @__PURE__ */ new WeakMap();
809
+ this.dirtyMessages.clear();
810
+ this.auditDisplays.clear();
811
+ this.structureDirty = true;
529
812
  this.restoreAllDisplays();
530
813
  restoreTurnStatus(this.turnStatusTexts);
531
814
  this.flow = next;
@@ -534,6 +817,8 @@ var __dshcfBundle = (() => {
534
817
  const row = createProcessedRowElement(state.duration);
535
818
  row.addEventListener("click", () => {
536
819
  state.expanded = !state.expanded;
820
+ this.animatableKeys.add(state.key);
821
+ this.animatableSegmentBlocks.set(state.key, new Set(state.snapshot.blocks.map((block) => block.key)));
537
822
  if (state.expanded) {
538
823
  for (const block of state.snapshot.blocks) {
539
824
  this.blockExpanded.set(block.key, false);
@@ -564,7 +849,11 @@ var __dshcfBundle = (() => {
564
849
  return;
565
850
  }
566
851
  let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
567
- if (target === null || target.parentElement !== flow) return;
852
+ if (target === null || target.parentElement !== flow) {
853
+ row.remove();
854
+ state.row = null;
855
+ return;
856
+ }
568
857
  while (target.previousElementSibling?.classList.contains("dshcf-flow-chip") === true) {
569
858
  target = target.previousElementSibling;
570
859
  }
@@ -572,40 +861,68 @@ var __dshcfBundle = (() => {
572
861
  }
573
862
  reconcileBlock(block, segment, desiredHidden) {
574
863
  const state = segment === null ? void 0 : this.segmentStates.get(segment.key);
864
+ const segmentAnimatableBlocks = segment === null ? void 0 : this.animatableSegmentBlocks.get(segment.key);
865
+ const animate = this.animatableKeys.has(block.key) || segment !== null && this.animatableKeys.has(segment.key) && (segmentAnimatableBlocks === void 0 || segmentAnimatableBlocks.has(block.key));
575
866
  const levelCollapsed = state !== void 0 && !state.expanded;
576
867
  if (levelCollapsed) {
577
- const existing = this.chips.get(block.key)?.chip;
578
- if (existing !== void 0 && existing.style.display !== "none") existing.style.display = "none";
579
- for (const row of block.rows) this.hideElement(row, desiredHidden);
580
- for (const container of block.containers) this.hideElement(container, desiredHidden);
581
- const keepHost = segment?.finalStep === block.host && hasBodyContent(block.host);
868
+ const keepHost = segment?.finalStep === block.host && this.hasBodyCached(block.host);
869
+ let hostFade = false;
582
870
  if (keepHost) this.restoreElement(block.host);
583
- else this.hideElement(block.host, desiredHidden);
584
- this.removeMergedThink(block.host);
871
+ else hostFade = this.hideElement(block.host, desiredHidden, animate);
872
+ for (const container of block.containers) this.hideElement(container, desiredHidden, animate);
873
+ for (const row of block.rows) this.hideElement(row, desiredHidden, animate);
874
+ const existing = this.chips.get(block.key)?.chip;
875
+ if (existing !== void 0 && existing.style.display !== "none") {
876
+ existing.style.marginBottom = "";
877
+ if (block.mount === "before" || keepHost) {
878
+ if (animate && this.canAnimate(existing)) this.startFadeCollapse(existing);
879
+ else existing.style.display = "none";
880
+ } else if (!hostFade) {
881
+ existing.style.display = "none";
882
+ }
883
+ }
884
+ this.releaseMergedThink(block.host, animate);
585
885
  return;
586
886
  }
587
- const chip = this.ensureChip(block);
588
- this.restoreElement(block.host);
589
- if (chip.style.display !== "") chip.style.display = "";
590
887
  let expanded = this.blockExpanded.get(block.key) ?? false;
591
888
  if (!expanded && block.rows.some((row) => row.hasAttribute("data-selected"))) {
592
889
  expanded = true;
593
890
  this.blockExpanded.set(block.key, true);
594
891
  }
595
- for (const row of block.rows) {
596
- if (expanded) this.restoreElement(row);
597
- else this.hideElement(row, desiredHidden);
598
- }
892
+ const chip = this.ensureChip(block);
893
+ const hostIsCollapsedRow = !expanded && block.rows.includes(block.host);
894
+ const hostWasHidden = block.host.style.display === "none";
895
+ const hostAnimate = !hostIsCollapsedRow && hostWasHidden && animate;
896
+ if (!hostIsCollapsedRow) this.restoreElement(block.host, hostAnimate);
897
+ const pendingChip = this.pendingAnims.get(chip);
898
+ if (pendingChip?.target === "hidden") this.cancelPendingSync(chip);
899
+ const chipWasHidden = chip.style.display === "none";
900
+ if (chip.style.display !== "") chip.style.display = "";
901
+ if (chipWasHidden && animate && !(hostAnimate && block.mount === "inside")) this.revealVisual(chip);
902
+ if (expanded || !this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
903
+ const chipSettle = () => {
904
+ if (!this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
905
+ };
599
906
  for (const container of block.containers) {
600
- if (expanded) this.restoreElement(container);
601
- else this.hideElement(container, desiredHidden);
907
+ if (expanded) this.restoreElement(container, animate);
908
+ else {
909
+ const started = this.hideElement(container, desiredHidden, animate, chipSettle);
910
+ if (started) this.pinChipMargin(chip);
911
+ }
912
+ }
913
+ for (const row of block.rows) {
914
+ if (expanded) this.restoreElement(row, animate);
915
+ else {
916
+ const started = this.hideElement(row, desiredHidden, animate, chipSettle);
917
+ if (started) this.pinChipMargin(chip);
918
+ }
602
919
  }
603
920
  if (expanded && block.rows.length > 1 && block.rows.every((row) => isThinkRow(row))) {
604
- this.syncMergedThink(block.host, block.rows, desiredHidden);
921
+ this.syncMergedThink(block.host, block.rows, desiredHidden, animate);
605
922
  } else {
606
- this.removeMergedThink(block.host);
923
+ if (this.releaseMergedThink(block.host, animate, chipSettle)) this.pinChipMargin(chip);
607
924
  }
608
- chip.classList.toggle("dshcf-has-body", block.mount === "inside" && hasBodyContent(block.host));
925
+ chip.classList.toggle("dshcf-has-body", block.mount === "inside" && this.hasBodyCached(block.host));
609
926
  updateChip(chip, block.rows, expanded);
610
927
  }
611
928
  ensureChip(block) {
@@ -623,14 +940,16 @@ var __dshcfBundle = (() => {
623
940
  chip2.setAttribute("data-dshcf-block-key", block.key);
624
941
  const leading = document.createElement("span");
625
942
  leading.className = "dshcf-leading";
626
- leading.appendChild(createTerminalIcon());
943
+ leading.appendChild(createCommandIcon());
627
944
  chip2.appendChild(leading);
628
945
  chip2.appendChild(createSpan("dshcf-chip-title"));
629
946
  chip2.appendChild(createSpan("dshcf-chip-sep"));
630
947
  chip2.appendChild(createSpan("dshcf-chip-summary"));
631
948
  chip2.appendChild(createChevronIcon("dshcf-chevron"));
949
+ chip2.style.display = "none";
632
950
  chip2.addEventListener("click", () => {
633
951
  this.blockExpanded.set(block.key, !(this.blockExpanded.get(block.key) ?? false));
952
+ this.animatableKeys.add(block.key);
634
953
  this.schedule();
635
954
  });
636
955
  record = { host: block.host, chip: chip2 };
@@ -648,7 +967,10 @@ var __dshcfBundle = (() => {
648
967
  }
649
968
  suppressBlock(block, desiredHidden) {
650
969
  const existing = this.chips.get(block.key)?.chip;
651
- if (existing !== void 0 && existing.style.display !== "none") existing.style.display = "none";
970
+ if (existing !== void 0 && existing.style.display !== "none") {
971
+ existing.style.marginBottom = "";
972
+ existing.style.display = "none";
973
+ }
652
974
  this.removeMergedThink(block.host);
653
975
  this.retainDisplayControl(block.host, desiredHidden);
654
976
  for (const row of block.rows) this.retainDisplayControl(row, desiredHidden);
@@ -668,7 +990,7 @@ var __dshcfBundle = (() => {
668
990
  }
669
991
  /** 连续思考合并行:插在第一个思考行前,标题用第一行思考内容;
670
992
  * 点击切换显示/隐藏全部原始思考行。 */
671
- syncMergedThink(host, rows, desiredHidden) {
993
+ syncMergedThink(host, rows, desiredHidden, animate = false) {
672
994
  let row = this.mergedThinks.get(host);
673
995
  if (row === void 0 || !row.isConnected) {
674
996
  row = document.createElement("button");
@@ -682,14 +1004,21 @@ var __dshcfBundle = (() => {
682
1004
  title.className = "dshcf-merged-title";
683
1005
  const chevron = createChevronIcon("dshcf-chevron");
684
1006
  row.append(leading, title, chevron);
1007
+ row.style.display = "none";
685
1008
  const btn = row;
686
1009
  btn.addEventListener("click", () => {
1010
+ if (this.mergedThinks.get(host) !== btn) return;
687
1011
  const next = !this.mergedExpanded.has(host);
688
- if (next) this.mergedExpanded.add(host);
689
- else this.mergedExpanded.delete(host);
690
- btn.setAttribute("aria-expanded", String(next));
691
- if (next) this.expandMergedBody(host, btn);
692
- else this.collapseMergedBody(host);
1012
+ if (next) {
1013
+ if (this.expandMergedBody(host, btn)) {
1014
+ this.mergedExpanded.add(host);
1015
+ btn.setAttribute("aria-expanded", "true");
1016
+ }
1017
+ } else {
1018
+ this.mergedExpanded.delete(host);
1019
+ btn.setAttribute("aria-expanded", "false");
1020
+ this.collapseMergedBody(host);
1021
+ }
693
1022
  });
694
1023
  rows[0].before(row);
695
1024
  this.mergedThinks.set(host, row);
@@ -711,42 +1040,155 @@ var __dshcfBundle = (() => {
711
1040
  }
712
1041
  const expanded = this.mergedExpanded.has(host);
713
1042
  if (row.getAttribute("aria-expanded") !== String(expanded)) row.setAttribute("aria-expanded", String(expanded));
1043
+ const rowWasHidden = row.style.display === "none";
714
1044
  if (row.style.display !== "") row.style.display = "";
1045
+ if (rowWasHidden && animate) this.revealVisual(row);
715
1046
  for (const r of rows) this.hideElement(r, desiredHidden);
716
- if (expanded) this.ensureMergedBody(host, row, false);
1047
+ if (expanded) {
1048
+ const result = this.ensureMergedBody(host, row, false);
1049
+ if (result !== null && result.created && animate) this.revealMergedBody(result.body);
1050
+ }
717
1051
  }
718
1052
  /** 展开合并行:直接读各思考行文本合成内容块(不依赖原生行展开:
719
- * 程序化 click 不触发 React 展开,且后台 tab 的 rAF 不执行)。 */
1053
+ * 程序化 click 不触发 React 展开,且后台 tab 的 rAF 不执行)。
1054
+ * 返回是否成功——思考行已不可读(parts 为空)时返回 false,调用方
1055
+ * 据此保持收起态,避免展开状态与内容块脱节。 */
720
1056
  expandMergedBody(host, btn) {
721
1057
  const cached = this.mergedBodyTexts.get(host);
722
- if (cached !== void 0) {
723
- this.ensureMergedBody(host, btn, true);
724
- return;
1058
+ if (cached === void 0) {
1059
+ const parts = this.currentThinkRows(host).map((r) => r.textContent.replace(/^Think\s*/, "").trim()).filter(Boolean);
1060
+ if (parts.length === 0) return false;
1061
+ this.mergedBodyTexts.set(host, parts.join("\n\n"));
1062
+ }
1063
+ const result = this.ensureMergedBody(host, btn, true);
1064
+ if (result === null) return false;
1065
+ if (result.created) {
1066
+ this.revealMergedBody(result.body);
1067
+ } else {
1068
+ this.cancelPendingSync(result.body);
725
1069
  }
726
- const parts = this.currentThinkRows(host).map((r) => r.textContent.replace(/^Think\s*/, "").trim()).filter(Boolean);
727
- if (parts.length === 0) return;
728
- this.mergedBodyTexts.set(host, parts.join("\n\n"));
729
- this.ensureMergedBody(host, btn, true);
1070
+ return true;
730
1071
  }
731
- /** 创建/更新合并内容块(缓存优先,不重新展开原生行)。 */
1072
+ /** 创建/更新合并内容块(缓存优先,不重新展开原生行)。
1073
+ * 返回内容块与其是否为本次新建(新建才走展开动画)。 */
732
1074
  ensureMergedBody(host, btn, force) {
733
1075
  const cached = this.mergedBodyTexts.get(host);
734
- if (cached === void 0) return;
1076
+ if (cached === void 0) return null;
735
1077
  let body = btn.nextElementSibling;
1078
+ let created = false;
736
1079
  if (body === null || !body.classList.contains("dshcf-merged-body")) {
737
- body = document.createElement("div");
738
- body.className = "dshcf-merged-body";
739
- btn.after(body);
1080
+ const next = document.createElement("div");
1081
+ next.className = "dshcf-merged-body";
1082
+ btn.after(next);
1083
+ body = next;
1084
+ created = true;
740
1085
  }
741
1086
  if (force || body.textContent !== cached) body.textContent = cached;
1087
+ return { body, created };
742
1088
  }
743
- /** 收起合并行:移除内容块(原生行保持隐藏)。 */
1089
+ /** 清理合并 think 行(v12):状态 map 立即清除;DOM 在手势动画路径下
1090
+ * 渐隐后移除(settle 回调),其余路径瞬删。渐隐中途被反向取消时元素
1091
+ * 保留,由后续 pass 的 syncMergedThink 重建/复用。
1092
+ * settle 透传给每个渐隐目标的移除回调之后(chip 间距钉住的结算探测点,
1093
+ * AI 评审 P0:merged 行渐隐不走 block.rows,必须纳入同一钉住体系)。 */
1094
+ releaseMergedThink(host, animate = false, settle) {
1095
+ const row = this.mergedThinks.get(host);
1096
+ this.mergedExpanded.delete(host);
1097
+ this.mergedBodyTexts.delete(host);
1098
+ if (row === void 0) return false;
1099
+ this.mergedThinks.delete(host);
1100
+ const body = row.nextElementSibling;
1101
+ const targets = body !== null && body.classList.contains("dshcf-merged-body") ? [row, body] : [row];
1102
+ if (animate && this.canAnimate(row)) {
1103
+ for (const t of targets) this.startFadeCollapse(t, () => {
1104
+ t.remove();
1105
+ settle?.();
1106
+ });
1107
+ return true;
1108
+ } else {
1109
+ for (const t of targets) t.remove();
1110
+ return false;
1111
+ }
1112
+ }
1113
+ /** merged-body 展开高度动画(机制样板:插件全资 DOM)。
1114
+ * 关键帧含 marginBottom 0→16px——其 CSS 有常量 margin-bottom:16px,
1115
+ * 高度从 0 起步时这 16px 会先占位产生小跳变。fill:'forwards' 托住终态,
1116
+ * onfinish 清内联后 cancel 释放,无闪烁窗口。收起由 collapseMergedBody
1117
+ * 做镜像高度卷下(同款账本与身份守卫),开合对称。 */
1118
+ revealMergedBody(body) {
1119
+ if (!this.canAnimate(body)) return;
1120
+ this.cancelPendingSync(body);
1121
+ const targetHeight = body.getBoundingClientRect().height;
1122
+ if (!(targetHeight > 0)) return;
1123
+ body.style.height = "0px";
1124
+ body.style.overflow = "hidden";
1125
+ body.style.marginBottom = "0px";
1126
+ const anim = body.animate(
1127
+ [
1128
+ { height: "0px", marginBottom: "0px" },
1129
+ { height: `${targetHeight}px`, marginBottom: "16px" }
1130
+ ],
1131
+ { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1132
+ );
1133
+ const record = { anim, target: "visible", kind: "height" };
1134
+ this.pendingAnims.set(body, record);
1135
+ anim.onfinish = () => {
1136
+ if (this.pendingAnims.get(body) !== record) return;
1137
+ this.pendingAnims.delete(body);
1138
+ body.style.height = "";
1139
+ body.style.overflow = "";
1140
+ body.style.marginBottom = "";
1141
+ anim.cancel();
1142
+ this.schedule();
1143
+ };
1144
+ anim.oncancel = () => {
1145
+ if (this.pendingAnims.get(body) !== record) return;
1146
+ this.pendingAnims.delete(body);
1147
+ };
1148
+ }
1149
+ /** 收起合并行:内容块高度卷下后移除——镜像 revealMergedBody 的唯一几何动画,
1150
+ * 开合对称。插件全资静态文本 DOM、无 React 协调竞争,可安全做几何收起
1151
+ * (与 seat 级拒绝盲卷的场景不同:那里是 React 混杂多卡片)。
1152
+ * reduced-motion / 无 WAAPI / 零高度降级为同步 remove()。 */
744
1153
  collapseMergedBody(host) {
745
1154
  const btn = this.mergedThinks.get(host);
746
- if (btn !== void 0) {
747
- const body = btn.nextElementSibling;
748
- if (body !== null && body.classList.contains("dshcf-merged-body")) body.remove();
1155
+ if (btn === void 0) return;
1156
+ const body = btn.nextElementSibling;
1157
+ if (body === null || !body.classList.contains("dshcf-merged-body")) return;
1158
+ const el = body;
1159
+ if (!this.canAnimate(el)) {
1160
+ el.remove();
1161
+ return;
749
1162
  }
1163
+ this.cancelPendingSync(el);
1164
+ const current = el.getBoundingClientRect().height;
1165
+ if (!(current > 0)) {
1166
+ el.remove();
1167
+ return;
1168
+ }
1169
+ el.style.height = `${current}px`;
1170
+ el.style.overflow = "hidden";
1171
+ const anim = el.animate(
1172
+ [
1173
+ { height: `${current}px`, marginBottom: "16px" },
1174
+ { height: "0px", marginBottom: "0px" }
1175
+ ],
1176
+ { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1177
+ );
1178
+ const record = { anim, target: "hidden", kind: "height" };
1179
+ this.pendingAnims.set(el, record);
1180
+ anim.onfinish = () => {
1181
+ if (this.pendingAnims.get(el) !== record) return;
1182
+ this.pendingAnims.delete(el);
1183
+ el.remove();
1184
+ anim.cancel();
1185
+ this.schedule();
1186
+ };
1187
+ anim.oncancel = () => {
1188
+ if (this.pendingAnims.get(el) !== record) return;
1189
+ this.pendingAnims.delete(el);
1190
+ this.clearCollapseLock(el);
1191
+ };
750
1192
  }
751
1193
  /** 当前宿主内的思考行(现取,React 重渲染后引用仍然有效)。 */
752
1194
  currentThinkRows(host) {
@@ -767,29 +1209,242 @@ var __dshcfBundle = (() => {
767
1209
  this.mergedExpanded.delete(host);
768
1210
  this.mergedBodyTexts.delete(host);
769
1211
  }
770
- hideElement(el, desired) {
771
- if (!this.originalDisplay.has(el) && !isDisplayed(el)) return;
772
- if (!this.originalDisplay.has(el)) this.originalDisplay.set(el, el.style.display);
773
- this.controlledDisplay.add(el);
1212
+ /** 正文判定(带缓存):同一消息子树未变时直接复用上次结果。失效由
1213
+ * markDirty(mutation 定向)与 switchFlow(整体重置)驱动;缓存的是
1214
+ * 纯文本/媒体存在性判定,与 display 状态无关,插件自身的显隐切换
1215
+ * 不会产生脏数据。 */
1216
+ hasBodyCached(el) {
1217
+ const cached = this.bodyTextCache.get(el);
1218
+ if (cached !== void 0) return cached;
1219
+ const value = hasBodyContent(el);
1220
+ this.bodyTextCache.set(el, value);
1221
+ return value;
1222
+ }
1223
+ /** 本块是否有在途收起渐隐(rows/containers/merged 行/body 任一)。
1224
+ * 基于 pendingAnims 账本无状态判定:onfinish/oncancel 都会即时清账,
1225
+ * 取消路径天然解锁(计数器/最后注册者会卡死)。merged 行渐隐时已被
1226
+ * releaseMergedThink 摘出 mergedThinks,按 DOM 类名现查。 */
1227
+ hasPendingCollapse(block) {
1228
+ const check = (el) => el !== null && el !== void 0 && this.pendingAnims.get(el)?.target === "hidden";
1229
+ if (block.containers.some(check)) return true;
1230
+ if (block.rows.some(check)) return true;
1231
+ const mergedRow = block.host.querySelector(".dshcf-merged-think");
1232
+ if (check(mergedRow)) return true;
1233
+ const mergedBody = mergedRow?.nextElementSibling;
1234
+ if (mergedBody instanceof HTMLElement && check(mergedBody)) return true;
1235
+ return false;
1236
+ }
1237
+ /** 钉住 chip 与首行的 16px 间距(收起 fade 期间;内联优先于 aria=false 的 0)。
1238
+ * flow-chip(context 等 before-mounted)豁免:其间距由宿主 row-gap 16px
1239
+ * 提供、自身 CSS 恒 0,钉住 16px 会叠加成 32px(真机实测:收起上下文
1240
+ * 注入时二级与三级间距瞬间扩大)。
1241
+ */
1242
+ pinChipMargin(chip) {
1243
+ if (chip.classList.contains("dshcf-flow-chip")) return;
1244
+ if (chip.style.marginBottom !== "16px") chip.style.marginBottom = "16px";
1245
+ }
1246
+ /** 解除钉住(aria=true 的 16px 或 aria=false 的 0 由 CSS 接管)。 */
1247
+ unpinChipMargin(chip) {
1248
+ if (chip.style.marginBottom !== "") chip.style.marginBottom = "";
1249
+ }
1250
+ /** 外部介入检测(issue #11 Bug A):当前内联值 ≠ 插件最后确保值,或所有权
1251
+ * 哨兵被 style 整体改写抹除。返回 true 时调用方放弃本次写回并交还账本——
1252
+ * 属性级改写由值比对捕获,整体改写(cssText / setAttribute('style'))由
1253
+ * 哨兵缺失捕获,两层合起来覆盖外部介入的两种形态。 */
1254
+ displayForeign(el) {
1255
+ const written = this.writtenDisplay.get(el);
1256
+ if (written === void 0) return false;
1257
+ return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
1258
+ }
1259
+ /** 清空单个元素的显示账本(三账本 + 所有权哨兵)。 */
1260
+ releaseDisplayLedger(el) {
1261
+ this.originalDisplay.delete(el);
1262
+ this.writtenDisplay.delete(el);
1263
+ this.controlledDisplay.delete(el);
1264
+ el.style.removeProperty(DISPLAY_OWNED_PROP);
1265
+ }
1266
+ /** 返回 true 表示启动了渐隐动画(调用方可据此决定内部元素的处置)。
1267
+ * settle 在渐隐自然结束时调用(onfinish 链;反向取消不触发)。 */
1268
+ hideElement(el, desired, animate = false, settle) {
774
1269
  desired.add(el);
775
- if (el.style.display !== "none") el.style.display = "none";
1270
+ const pending = this.pendingAnims.get(el);
1271
+ if (pending !== void 0) {
1272
+ if (pending.target === "hidden") return false;
1273
+ this.cancelPendingSync(el);
1274
+ }
1275
+ if (!this.originalDisplay.has(el) && !isDisplayed(el)) return false;
1276
+ if (this.hasAnimatingAncestor(el)) return false;
1277
+ if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
1278
+ this.originalDisplay.set(el, el.style.display);
1279
+ this.writtenDisplay.set(el, el.style.display);
1280
+ el.style.setProperty(DISPLAY_OWNED_PROP, "1");
1281
+ }
1282
+ this.controlledDisplay.add(el);
1283
+ if (el.style.display === "none") return false;
1284
+ if (animate && this.canAnimate(el)) {
1285
+ this.startFadeCollapse(el, settle);
1286
+ return true;
1287
+ }
1288
+ el.style.display = "none";
1289
+ this.writtenDisplay.set(el, "none");
1290
+ return false;
776
1291
  }
777
- restoreElement(el) {
1292
+ restoreElement(el, animate = false) {
1293
+ const pending = this.pendingAnims.get(el);
1294
+ if (pending !== void 0) {
1295
+ if (pending.target === "visible") return;
1296
+ this.cancelPendingSync(el);
1297
+ }
778
1298
  if (!this.originalDisplay.has(el)) return;
1299
+ if (this.displayForeign(el)) {
1300
+ this.releaseDisplayLedger(el);
1301
+ return;
1302
+ }
779
1303
  const original = this.originalDisplay.get(el);
1304
+ if (!animate || !this.canAnimate(el) || this.hasAnimatingAncestor(el)) {
1305
+ if (el.style.display !== original) el.style.display = original;
1306
+ this.releaseDisplayLedger(el);
1307
+ return;
1308
+ }
780
1309
  if (el.style.display !== original) el.style.display = original;
781
- this.originalDisplay.delete(el);
782
- this.controlledDisplay.delete(el);
1310
+ this.writtenDisplay.set(el, original);
1311
+ this.startReveal(el);
1312
+ }
1313
+ /** 是否可动画:WAAPI 特性检测 + reduced-motion 门控(均做 typeof 防桩缺失)。 */
1314
+ canAnimate(el) {
1315
+ if (typeof el.animate !== "function") return false;
1316
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
1317
+ return true;
1318
+ }
1319
+ /** 展开方向淡入(opacity + 4px 微位移):无高度分量、零布局读取。
1320
+ * onfinish 按终态可见对齐账本(双删除)并 schedule() 幂等重同步;
1321
+ * oncancel 只做身份守卫删除——取消方的终态写入自己负责。 */
1322
+ startReveal(el) {
1323
+ const anim = el.animate(
1324
+ [
1325
+ { opacity: "0", transform: "translateY(4px)" },
1326
+ { opacity: "1", transform: "translateY(0)" }
1327
+ ],
1328
+ { duration: ANIM_DURATION_MS, easing: ANIM_EASING }
1329
+ );
1330
+ const record = { anim, target: "visible", kind: "fade" };
1331
+ this.pendingAnims.set(el, record);
1332
+ anim.onfinish = () => {
1333
+ if (this.pendingAnims.get(el) !== record) return;
1334
+ this.pendingAnims.delete(el);
1335
+ this.releaseDisplayLedger(el);
1336
+ this.schedule();
1337
+ };
1338
+ anim.oncancel = () => {
1339
+ if (this.pendingAnims.get(el) !== record) return;
1340
+ this.pendingAnims.delete(el);
1341
+ };
1342
+ }
1343
+ /** 同步取消在途动画并清账:收起动画需同时清锁高内联(height/overflow/
1344
+ * marginBottom),否则取消方写完终态后元素仍被锁高裁剪一帧以上。 */
1345
+ cancelPendingSync(el) {
1346
+ const pending = this.pendingAnims.get(el);
1347
+ if (pending === void 0) return;
1348
+ pending.anim.cancel();
1349
+ this.pendingAnims.delete(el);
1350
+ if (pending.kind === "height") this.clearCollapseLock(el);
1351
+ }
1352
+ clearCollapseLock(el) {
1353
+ el.style.height = "";
1354
+ el.style.overflow = "";
1355
+ el.style.marginBottom = "";
1356
+ el.style.boxSizing = "";
1357
+ }
1358
+ /** 祖先 seat 在途动画检测:沿 parentNode 走到 flow,任一祖先在 pendingAnims
1359
+ * 即视为在途。分层规则——同一视觉变化只动画一层。 */
1360
+ hasAnimatingAncestor(el) {
1361
+ const flow = this.flow;
1362
+ if (flow === null) return false;
1363
+ let node = el.parentElement;
1364
+ while (node !== null && node !== flow) {
1365
+ if (this.pendingAnims.has(node)) return true;
1366
+ node = node.parentElement;
1367
+ }
1368
+ return false;
1369
+ }
1370
+ /** 收起方向渐隐动画(v11 定稿):镜像 reveal 的 opacity + 4px 微位移,
1371
+ * 淡完 onfinish 写 display:none 并保持双条目(镜像 hideElement 终态契约)。
1372
+ * fill:'forwards' 占位到终态写入后释放;无几何锁、无 gap 补偿。 */
1373
+ startFadeCollapse(el, settle) {
1374
+ const anim = el.animate(
1375
+ [
1376
+ { opacity: "1", transform: "translateY(0)" },
1377
+ { opacity: "0", transform: "translateY(4px)" }
1378
+ ],
1379
+ { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1380
+ );
1381
+ const record = { anim, target: "hidden", kind: "fade" };
1382
+ 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
+ };
1398
+ anim.oncancel = () => {
1399
+ if (this.pendingAnims.get(el) !== record) return;
1400
+ this.pendingAnims.delete(el);
1401
+ };
1402
+ }
1403
+ /** 轻量视觉 reveal(opacity + 4px 微位移):用于插件全资元素的即时显示
1404
+ * 路径——chip(一级展开时出现)与 merged-think 行(二级展开时出现)。
1405
+ * 这些元素的 display 完全由插件直写、无 React 协调竞争,因此不入
1406
+ * pendingAnims 账本、无仲裁;收起同为直写 display:none,无 fill 的在途
1407
+ * 动画残留在隐藏元素上自然失效。门控沿用 animate 布尔(手势路径才调)。 */
1408
+ revealVisual(el) {
1409
+ if (!this.canAnimate(el)) return;
1410
+ el.animate(
1411
+ [
1412
+ { opacity: "0", transform: "translateY(4px)" },
1413
+ { opacity: "1", transform: "translateY(0)" }
1414
+ ],
1415
+ { duration: ANIM_DURATION_MS, easing: ANIM_EASING }
1416
+ );
783
1417
  }
784
1418
  restoreUnusedDisplays(desired) {
785
1419
  for (const el of [...this.controlledDisplay]) {
786
1420
  if (!desired.has(el)) this.restoreElement(el);
787
1421
  }
788
1422
  }
1423
+ /** 收集 audit 需要观察的 display 集合;只读取内联样式,不触发布局计算。 */
1424
+ collectAuditDisplays(flow) {
1425
+ const nodes = new Set(flowItems(flow));
1426
+ for (const el of this.controlledDisplay) {
1427
+ if (el.isConnected && nodeWithin(el, flow)) nodes.add(el);
1428
+ }
1429
+ for (const { chip } of this.chips.values()) {
1430
+ if (chip.isConnected && nodeWithin(chip, flow)) nodes.add(chip);
1431
+ }
1432
+ for (const row of this.mergedThinks.values()) {
1433
+ if (row.isConnected && nodeWithin(row, flow)) nodes.add(row);
1434
+ }
1435
+ const displays = /* @__PURE__ */ new Map();
1436
+ for (const el of nodes) displays.set(el, el.style.display);
1437
+ return displays;
1438
+ }
1439
+ /** 在完整 pass 完成后保存 display 基线,供下一次轻量 audit 比对。 */
1440
+ captureAuditDisplays(flow) {
1441
+ this.auditDisplays = this.collectAuditDisplays(flow);
1442
+ }
789
1443
  restoreAllDisplays() {
790
1444
  for (const el of [...this.controlledDisplay]) this.restoreElement(el);
791
1445
  this.controlledDisplay.clear();
792
1446
  this.originalDisplay = /* @__PURE__ */ new WeakMap();
1447
+ this.writtenDisplay = /* @__PURE__ */ new WeakMap();
793
1448
  }
794
1449
  };
795
1450
  function createSpan(cls) {
@@ -797,28 +1452,20 @@ var __dshcfBundle = (() => {
797
1452
  span.className = cls;
798
1453
  return span;
799
1454
  }
800
- function createTerminalIcon() {
801
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
802
- svg.setAttribute("viewBox", "0 0 12 10");
803
- svg.setAttribute("width", "12");
804
- svg.setAttribute("height", "10");
805
- const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
806
- rect.setAttribute("x", "0.5");
807
- rect.setAttribute("y", "0.5");
808
- rect.setAttribute("width", "11");
809
- rect.setAttribute("height", "9");
810
- rect.setAttribute("rx", "1.5");
811
- rect.setAttribute("fill", "none");
812
- rect.setAttribute("stroke", "currentColor");
813
- const prompt = document.createElementNS("http://www.w3.org/2000/svg", "text");
814
- prompt.setAttribute("x", "2");
815
- prompt.setAttribute("y", "7.5");
816
- prompt.setAttribute("font-size", "7");
817
- prompt.setAttribute("fill", "currentColor");
818
- prompt.textContent = ">_";
819
- svg.append(rect, prompt);
820
- return svg;
821
- }
1455
+ var COMMAND_ICON_PATHS = [
1456
+ {
1457
+ transform: "translate(0.6689 1.073)",
1458
+ d: "M11.4818 5.57813C11.4818 4.45301 11.4807 3.66237 11.4075 3.05908C11.3359 2.46953 11.2024 2.13852 10.9939 1.89441C10.9247 1.81341 10.8493 1.73801 10.7683 1.66882C10.5242 1.46033 10.1932 1.32686 9.60364 1.25525C9.00034 1.18198 8.20974 1.18091 7.0846 1.18091L5.57813 1.18091C4.45301 1.18091 3.66238 1.18198 3.05908 1.25525C2.46953 1.32686 2.13852 1.46033 1.89441 1.66882C1.81341 1.73801 1.73801 1.81341 1.66882 1.89441C1.46033 2.13852 1.32686 2.46953 1.25525 3.05908C1.18198 3.66238 1.18091 4.45301 1.18091 5.57813L1.18091 6.2771C1.18091 7.40218 1.18197 8.19288 1.25525 8.79614C1.32687 9.38553 1.46036 9.71674 1.66882 9.96082C1.73797 10.0417 1.81347 10.1173 1.89441 10.1864C2.13851 10.3948 2.13965 10.5275 3.05908 10.5991C3.66238 10.6724 4.45298 10.6735 5.57813 10.6735L7.0846 10.6735C8.20977 10.6735 9.00033 10.6724 9.60364 10.5991C10.1931 10.5275 10.5242 10.3948 10.7683 10.1864C10.8493 10.1173 10.9247 10.0417 10.9939 9.96082C11.2024 9.71674 11.3358 9.38553 11.4075 8.79614C11.4808 8.19288 11.4818 7.40218 11.4818 6.2771L11.4818 5.57813ZM12.6627 6.2771C12.6627 7.37222 12.6637 8.247 12.5798 8.93799C12.4942 9.64284 12.3133 10.2359 11.8928 10.7282C11.7834 10.8562 11.6637 10.9751 11.5356 11.0845C11.0434 11.5049 10.4511 11.6867 9.74634 11.7723C9.05525 11.8563 8.17999 11.8552 7.0846 11.8552L5.57813 11.8552C4.48273 11.8552 3.60747 11.8563 2.91638 11.7723C2.21157 11.6867 1.61933 11.5049 1.12708 11.0845C0.99901 10.9751 0.879281 10.8562 0.769898 10.7282C0.349454 10.2359 0.168506 9.64284 0.0828864 8.93799C-0.00101964 8.247 4.88512e-07 7.37222 6.47206e-07 6.2771L6.47206e-07 5.57813C6.47206e-07 4.48273 -0.00106163 3.60747 0.0828864 2.91638C0.168502 2.21168 0.349594 1.61928 0.769898 1.12708C0.879302 0.998981 0.998981 0.879302 1.12708 0.769898C1.61928 0.349594 2.21168 0.168502 2.91638 0.0828864C3.60747 -0.00106163 4.48273 6.47206e-07 5.57813 6.47206e-07L7.0846 6.47206e-07C8.17999 6.47206e-07 9.05525 -0.00106163 9.74634 0.0828864C10.451 0.168505 11.0434 0.349587 11.5356 0.769898C11.6637 0.879302 11.7834 0.998981 11.8928 1.12708C12.3131 1.61928 12.4942 2.21169 12.5798 2.91638C12.6638 3.60747 12.6627 4.48273 12.6627 5.57813L12.6627 6.2771Z"
1459
+ },
1460
+ {
1461
+ transform: "translate(0.6689 1.073)",
1462
+ d: "M6.02607 5.50955L6.44306 5.9274L3.84284 8.52762L3.425 8.11063L3.00715 7.69278L4.77253 5.9274L3.00715 4.16202L3.84284 3.32633L6.02607 5.50955Z"
1463
+ },
1464
+ {
1465
+ transform: "translate(0.6689 1.073)",
1466
+ d: "M9.23789 7.35397L9.23789 8.53488L6.96238 8.53488L6.96238 7.35397L9.23789 7.35397Z"
1467
+ }
1468
+ ];
822
1469
  var NATIVE_CHEVRON_DOWN_PATH = "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
823
1470
  function createChevronIcon(className) {
824
1471
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -886,10 +1533,31 @@ var __dshcfBundle = (() => {
886
1533
  const vb = (svg.getAttribute("viewBox") ?? "").trim().split(/\s+/);
887
1534
  return vb.length === 4 && Number(vb[2]) === 14 && Number(vb[3]) === 14;
888
1535
  }
1536
+ var cachedNativeCommandSvg = null;
889
1537
  function createCommandIcon() {
1538
+ if (cachedNativeCommandSvg !== null) return cachedNativeCommandSvg.cloneNode(true);
890
1539
  const native = findNativeCommandSvg();
891
- if (native !== null) return native.cloneNode(true);
892
- return createTerminalIcon();
1540
+ if (native !== null) {
1541
+ cachedNativeCommandSvg = native;
1542
+ return native.cloneNode(true);
1543
+ }
1544
+ return createCommandIconFallback();
1545
+ }
1546
+ function createCommandIconFallback() {
1547
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1548
+ svg.setAttribute("viewBox", "0 0 14 14");
1549
+ svg.setAttribute("width", "14");
1550
+ svg.setAttribute("height", "14");
1551
+ svg.setAttribute("fill", "none");
1552
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1553
+ for (const p of COMMAND_ICON_PATHS) {
1554
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1555
+ path.setAttribute("transform", p.transform);
1556
+ path.setAttribute("d", p.d);
1557
+ path.setAttribute("fill", "currentColor");
1558
+ svg.appendChild(path);
1559
+ }
1560
+ return svg;
893
1561
  }
894
1562
  var CONTEXT_ICON_PATHS = [
895
1563
  {
@@ -928,13 +1596,41 @@ var __dshcfBundle = (() => {
928
1596
  }
929
1597
  return svg;
930
1598
  }
1599
+ var WRITE_ICON_PATH = "M9.94076 1.34942C10.7047 0.90231 11.6503 0.902415 12.4143 1.34942C12.7061 1.52015 12.9688 1.79118 13.3104 2.13284C13.6521 2.47448 13.9231 2.73721 14.0939 3.02894C14.5408 3.79294 14.5409 4.73856 14.0939 5.50251C13.9231 5.79415 13.652 6.05704 13.3104 6.39861L6.65932 13.0497C6.28068 13.4284 6.00695 13.7108 5.66543 13.9097C5.32391 14.1085 4.94315 14.2074 4.42705 14.3498L3.24394 14.6761C2.77527 14.8054 2.34538 14.9262 2.00131 14.9684C1.65196 15.0112 1.17964 15.0013 0.810764 14.6325C0.441921 14.2637 0.432107 13.7913 0.47486 13.442C0.517035 13.0979 0.6379 12.668 0.767181 12.1993L1.09352 11.0162C1.23588 10.5001 1.33481 10.1193 1.5336 9.77784C1.7325 9.43632 2.0149 9.1626 2.39355 8.78395L9.04466 2.13284C9.38625 1.79126 9.64911 1.52016 9.94076 1.34942ZM15.5427 14.8398H7.55223L8.96707 13.425H15.5427V14.8398ZM3.39382 9.78422C2.965 10.213 2.84244 10.3436 2.75709 10.49C2.67183 10.6366 2.61862 10.8079 2.45733 11.3925L2.13099 12.5756C2.00183 13.0439 1.92194 13.3419 1.88863 13.5536C2.10041 13.5204 2.39872 13.4416 2.86764 13.3123L4.05075 12.9859C4.63544 12.8246 4.80669 12.7715 4.95323 12.6862C5.09968 12.6008 5.23022 12.4783 5.65905 12.0494L10.721 6.98644L8.45577 4.72121L3.39382 9.78422ZM11.7 2.57079C11.3774 2.38198 10.9777 2.38198 10.6551 2.57079C10.5602 2.62647 10.4487 2.72931 10.0449 3.13311L9.45604 3.72094L11.7213 5.98617L12.3102 5.39833C12.7139 4.99457 12.8168 4.88307 12.8725 4.78818C13.0613 4.46561 13.0612 4.06585 12.8725 3.74326C12.8169 3.64827 12.7146 3.53752 12.3102 3.13311C11.9057 2.72863 11.795 2.6264 11.7 2.57079Z";
1600
+ function findNativeWriteSvg() {
1601
+ const selector = '[data-tool="write"] [data-disclosure-row], [data-tool="edit"] [data-disclosure-row]';
1602
+ for (const drow of document.querySelectorAll(selector)) {
1603
+ for (const svg of drow.querySelectorAll("svg")) {
1604
+ if (svg.querySelectorAll("path").length === 1 && isIcon16(svg)) return svg;
1605
+ }
1606
+ }
1607
+ return null;
1608
+ }
1609
+ function isIcon16(svg) {
1610
+ const vb = (svg.getAttribute("viewBox") ?? "").trim().split(/\s+/);
1611
+ return vb.length === 4 && Number(vb[2]) === 16 && Number(vb[3]) === 16;
1612
+ }
1613
+ function createWriteIcon() {
1614
+ const native = findNativeWriteSvg();
1615
+ if (native !== null) return native.cloneNode(true);
1616
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1617
+ svg.setAttribute("viewBox", "0 0 16 16");
1618
+ svg.setAttribute("width", "14");
1619
+ svg.setAttribute("height", "14");
1620
+ svg.setAttribute("fill", "none");
1621
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1622
+ path.setAttribute("d", WRITE_ICON_PATH);
1623
+ path.setAttribute("fill", "currentColor");
1624
+ svg.appendChild(path);
1625
+ return svg;
1626
+ }
931
1627
  function syncLeadingIcon(chip, kind) {
932
1628
  const leading = chip.querySelector(".dshcf-leading");
933
1629
  if (leading === null) return;
934
1630
  const existing = leading.querySelector("svg");
935
1631
  if (existing !== null && existing.getAttribute("data-dshcf-icon") === kind) return;
936
1632
  for (const child of [...leading.childNodes]) child.remove();
937
- const svg = kind === "think" ? createThinkIcon() : kind === "context" ? createContextIcon() : createCommandIcon();
1633
+ const svg = kind === "think" ? createThinkIcon() : kind === "context" ? createContextIcon() : kind === "write" ? createWriteIcon() : createCommandIcon();
938
1634
  svg.setAttribute("data-dshcf-icon", kind);
939
1635
  leading.appendChild(svg);
940
1636
  }
@@ -951,6 +1647,15 @@ var __dshcfBundle = (() => {
951
1647
  }
952
1648
  return false;
953
1649
  }
1650
+ function isPluginOwnedNode(node) {
1651
+ const element = node.nodeType === 1 ? node : node.parentElement;
1652
+ return element !== null && element !== void 0 && element.closest(".dshcf-chip, .dshcf-processed, .dshcf-merged-think, .dshcf-merged-body") !== null;
1653
+ }
1654
+ function flowChildOwner(node, flow) {
1655
+ let current = node;
1656
+ while (current !== null && current.parentNode !== flow) current = current.parentNode;
1657
+ return current instanceof HTMLElement ? current : null;
1658
+ }
954
1659
  function flowItems(flow) {
955
1660
  return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains("dshcf-processed") && !el.classList.contains("dshcf-flow-chip"));
956
1661
  }
@@ -969,7 +1674,7 @@ var __dshcfBundle = (() => {
969
1674
  return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
970
1675
  });
971
1676
  }
972
- function buildSegments(flow, blocks) {
1677
+ function buildSegments(flow, blocks, hasBody) {
973
1678
  const items = flowItems(flow);
974
1679
  const itemIndex = new Map(items.map((el, index) => [el, index]));
975
1680
  const snapshots = [];
@@ -982,7 +1687,7 @@ var __dshcfBundle = (() => {
982
1687
  const segmentBlocks = blocks.filter((block) => inRange.has(block.host));
983
1688
  const bodySteps = range.filter((el) => {
984
1689
  const kind = el.getAttribute("data-chat-flow-kind");
985
- return (kind === "assistant-step" || kind === "assistant") && hasBodyContent(el);
1690
+ return (kind === "assistant-step" || kind === "assistant") && hasBody(el);
986
1691
  });
987
1692
  const finalStep = bodySteps.length > 0 ? bodySteps[bodySteps.length - 1] : null;
988
1693
  const middleSteps = new Set(bodySteps.slice(0, -1));
@@ -1043,7 +1748,7 @@ var __dshcfBundle = (() => {
1043
1748
  if (segment.finalStep !== null) workHosts.add(segment.finalStep);
1044
1749
  return [...workHosts].some(isDisplayed);
1045
1750
  }
1046
- function findBlocks(flow) {
1751
+ function findBlocks(flow, hasBody) {
1047
1752
  const blocks = [];
1048
1753
  const children = flowItems(flow);
1049
1754
  let run = null;
@@ -1080,7 +1785,7 @@ var __dshcfBundle = (() => {
1080
1785
  const workRows = [...callRowsIn(el), ...commandRowsIn(el)];
1081
1786
  const isToolPile = workRows.length > 0;
1082
1787
  const isContext = kind === "context";
1083
- const hasBody = !isToolPile ? hasBodyContent(el) : false;
1788
+ const msgHasBody = !isToolPile ? hasBody(el) : false;
1084
1789
  if (isContext) {
1085
1790
  flushCarry();
1086
1791
  if (run === null || run.category !== "context") run = makeBlock(el, "context");
@@ -1088,7 +1793,7 @@ var __dshcfBundle = (() => {
1088
1793
  run.mount = "before";
1089
1794
  continue;
1090
1795
  }
1091
- if (isToolPile || thinkRows.length > 0 && !hasBody) {
1796
+ if (isToolPile || thinkRows.length > 0 && !msgHasBody) {
1092
1797
  if (run === null || run.category !== "work") run = makeBlock(el, "work");
1093
1798
  if (carry.length > 0) {
1094
1799
  run.rows.push(...carry);
@@ -1100,7 +1805,7 @@ var __dshcfBundle = (() => {
1100
1805
  run.containers.push(el);
1101
1806
  }
1102
1807
  if (workRows.includes(el)) run.mount = "before";
1103
- } else if (el.hasAttribute("data-chat-anchor-key") && (thinkRows.length > 0 || hasBody) || hasBody && kind !== null) {
1808
+ } else if (el.hasAttribute("data-chat-anchor-key") && (thinkRows.length > 0 || msgHasBody) || msgHasBody && kind !== null) {
1104
1809
  flushCarry();
1105
1810
  if (thinkRows.length > 0) {
1106
1811
  const segments = splitThinkByBody(el, thinkRows);
@@ -1128,11 +1833,29 @@ var __dshcfBundle = (() => {
1128
1833
  return blocks;
1129
1834
  }
1130
1835
  function splitThinkByBody(el, rows) {
1836
+ const texts = [];
1837
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1838
+ let node;
1839
+ while ((node = walker.nextNode()) !== null) {
1840
+ if (node.data.trim() === "") continue;
1841
+ const parent = node.parentElement;
1842
+ if (parent !== null && parent.closest('[data-variant="think"], [data-chat-call-id], .dshcf-chip, .dshcf-merged-think, .dshcf-merged-body') !== null) continue;
1843
+ texts.push(node);
1844
+ }
1845
+ const hasBetween = (a, b) => {
1846
+ for (const t of texts) {
1847
+ const posA = a.compareDocumentPosition(t);
1848
+ if ((posA & Node.DOCUMENT_POSITION_FOLLOWING) === 0) continue;
1849
+ const posB = b.compareDocumentPosition(t);
1850
+ return (posB & Node.DOCUMENT_POSITION_PRECEDING) !== 0;
1851
+ }
1852
+ return false;
1853
+ };
1131
1854
  const segments = [];
1132
1855
  let current = [];
1133
1856
  for (let i = 0; i < rows.length; i++) {
1134
1857
  current.push(rows[i]);
1135
- if (i + 1 < rows.length && hasBodyBetween(el, rows[i], rows[i + 1])) {
1858
+ if (i + 1 < rows.length && hasBetween(rows[i], rows[i + 1])) {
1136
1859
  segments.push(current);
1137
1860
  current = [];
1138
1861
  }
@@ -1140,21 +1863,6 @@ var __dshcfBundle = (() => {
1140
1863
  if (current.length > 0) segments.push(current);
1141
1864
  return segments.length > 0 ? segments : [rows];
1142
1865
  }
1143
- function hasBodyBetween(el, a, b) {
1144
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1145
- let node;
1146
- while ((node = walker.nextNode()) !== null) {
1147
- if (node.data.trim() === "") continue;
1148
- const parent = node.parentElement;
1149
- if (parent !== null && parent.closest('[data-variant="think"], [data-chat-call-id], .dshcf-chip, .dshcf-merged-think, .dshcf-merged-body') !== null) continue;
1150
- const posA = a.compareDocumentPosition(node);
1151
- const posB = b.compareDocumentPosition(node);
1152
- if ((posA & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 && (posB & Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
1153
- return true;
1154
- }
1155
- }
1156
- return false;
1157
- }
1158
1866
  function hasBodyText(el) {
1159
1867
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1160
1868
  let node;
@@ -1269,7 +1977,6 @@ var __dshcfBundle = (() => {
1269
1977
  runningTool,
1270
1978
  runningThink,
1271
1979
  tools,
1272
- count: rows.length,
1273
1980
  hasError: infos.some((i) => i.state === "error"),
1274
1981
  hasStopped: infos.some((i) => i.state === "stopped"),
1275
1982
  allContext: infos.length > 0 && infos.every((i) => i.label === "\u4E0A\u4E0B\u6587\u6CE8\u5165")
@@ -1298,14 +2005,19 @@ var __dshcfBundle = (() => {
1298
2005
  titleText = "\u5DF2\u601D\u8003";
1299
2006
  summaryText = "";
1300
2007
  }
1301
- const kind = running !== null ? running.kind : info.allContext ? "context" : info.tools.length > 0 ? "tool" : "think";
2008
+ let kind = running !== null ? running.kind : info.allContext ? "context" : info.tools.length > 0 ? "tool" : "think";
2009
+ if (running === null && info.tools.some((tool) => tool === "Edit" || tool === "Write")) kind = "write";
1302
2010
  if (title.textContent !== titleText) title.textContent = titleText;
1303
2011
  if (summary.textContent !== summaryText) summary.textContent = summaryText;
1304
2012
  if (sep !== null) {
1305
2013
  const sepDisplay = summaryText === "" ? "none" : "";
1306
2014
  if (sep.style.display !== sepDisplay) sep.style.display = sepDisplay;
1307
2015
  }
1308
- summary.scrollLeft = running !== null ? summary.scrollWidth - summary.clientWidth : 0;
2016
+ if (running !== null) {
2017
+ summary.scrollLeft = summary.scrollWidth - summary.clientWidth;
2018
+ } else if (chip.classList.contains("running") && summary.scrollLeft !== 0) {
2019
+ summary.scrollLeft = 0;
2020
+ }
1309
2021
  const expandedAttr = String(expanded);
1310
2022
  if (chip.getAttribute("aria-expanded") !== expandedAttr) {
1311
2023
  chip.setAttribute("aria-expanded", expandedAttr);
@@ -1333,8 +2045,16 @@ var __dshcfBundle = (() => {
1333
2045
  function isThinkRow(row) {
1334
2046
  return row.matches('[data-variant="think"]') && !row.hasAttribute("data-tool");
1335
2047
  }
2048
+ var turnDurationCache = /* @__PURE__ */ new WeakMap();
1336
2049
  function parseTurnDuration(boundary) {
1337
2050
  const text = boundary.textContent ?? "";
2051
+ const cached = turnDurationCache.get(boundary);
2052
+ if (cached !== void 0 && cached.text === text) return cached.duration;
2053
+ const duration = parseTurnDurationText(boundary, text);
2054
+ turnDurationCache.set(boundary, { text, duration });
2055
+ return duration;
2056
+ }
2057
+ function parseTurnDurationText(boundary, text) {
1338
2058
  const m = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
1339
2059
  if (m !== null) {
1340
2060
  if (m[1] !== void 0 && m[2] !== void 0) return Number(m[1]) * 6e4 + Number(m[2]) * 1e3;
@@ -1403,21 +2123,29 @@ var __dshcfBundle = (() => {
1403
2123
  style.textContent = CHIP_CSS;
1404
2124
  document.head.appendChild(style);
1405
2125
  }
1406
- function replaceTurnStatus(flow, originals) {
2126
+ function replaceTurnStatus(flow, originals, statusText) {
1407
2127
  const statuses = flow.matches('[role="status"]') ? [flow, ...flow.querySelectorAll('[role="status"]')] : [...flow.querySelectorAll('[role="status"]')];
1408
2128
  for (const status of statuses) {
1409
2129
  for (const node of status.childNodes) {
1410
2130
  if (node instanceof Text && node.data.includes("Deep diving")) {
1411
- if (!originals.has(node)) originals.set(node, node.data);
1412
- const next = node.data.replace("Deep diving", "Deep sleeping");
1413
- if (node.data !== next) node.data = next;
2131
+ let record = originals.get(node);
2132
+ if (record === void 0) {
2133
+ record = { original: node.data, written: "" };
2134
+ originals.set(node, record);
2135
+ }
2136
+ if (node.data !== record.written) record.original = node.data;
2137
+ const next = node.data.replace(/Deep diving[.…]*/, statusText);
2138
+ if (node.data !== next) {
2139
+ node.data = next;
2140
+ record.written = next;
2141
+ }
1414
2142
  }
1415
2143
  }
1416
2144
  }
1417
2145
  }
1418
2146
  function restoreTurnStatus(originals) {
1419
- for (const [node, original] of originals) {
1420
- if (node.isConnected && node.data.includes("Deep sleeping")) node.data = original;
2147
+ for (const [node, record] of originals) {
2148
+ if (node.isConnected && node.data === record.written && node.data !== record.original) node.data = record.original;
1421
2149
  }
1422
2150
  originals.clear();
1423
2151
  }
@@ -1425,15 +2153,255 @@ var __dshcfBundle = (() => {
1425
2153
  document.getElementById(STYLE_ID)?.remove();
1426
2154
  }
1427
2155
 
2156
+ // src/settings.ts
2157
+ var AUTO_COLLAPSE_NS = "dsh-auto-collapse";
2158
+ var DEFAULT_STATUS_TEXT2 = "Deep sleeping...";
2159
+ function statusTextProvider(scope) {
2160
+ return () => {
2161
+ if (scope === void 0) return DEFAULT_STATUS_TEXT2;
2162
+ const snapshot = scope.getSnapshot();
2163
+ const value = snapshot.value;
2164
+ return value?.statusText ?? DEFAULT_STATUS_TEXT2;
2165
+ };
2166
+ }
2167
+ var CARD_CSS = `
2168
+ .dshcf-settings-card {
2169
+ border: 1px solid var(--dsw-alias-border-l2);
2170
+ background: var(--dsw-alias-bg-layer-3);
2171
+ border-radius: 12px;
2172
+ list-style: none;
2173
+ transition: border-color .16s, background .16s;
2174
+ }
2175
+ .dshcf-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }
2176
+ .dshcf-settings-cardOpen {
2177
+ background: var(--dsw-alias-bg-layer-2);
2178
+ border-color: var(--dsw-alias-label-dimmed);
2179
+ }
2180
+ .dshcf-settings-header {
2181
+ appearance: none;
2182
+ width: 100%;
2183
+ font: inherit;
2184
+ color: inherit;
2185
+ text-align: left;
2186
+ cursor: pointer;
2187
+ background: 0 0;
2188
+ border: 0;
2189
+ border-radius: 12px;
2190
+ align-items: center;
2191
+ gap: 12px;
2192
+ padding: 14px 16px;
2193
+ display: flex;
2194
+ }
2195
+ .dshcf-settings-header:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
2196
+ .dshcf-settings-headText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; display: flex; }
2197
+ .dshcf-settings-name { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
2198
+ .dshcf-settings-description { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }
2199
+ .dshcf-settings-chevron { color: var(--dsw-alias-label-tertiary); flex: none; transition: transform .16s; }
2200
+ .dshcf-settings-chevronOpen { transform: rotate(180deg); }
2201
+ .dshcf-settings-pending {
2202
+ white-space: nowrap;
2203
+ background: var(--dsw-alias-bg-module-platform);
2204
+ color: var(--dsw-alias-label-secondary);
2205
+ border-radius: 999px;
2206
+ flex: none;
2207
+ padding: 1px 8px;
2208
+ font-size: 11px;
2209
+ font-weight: 500;
2210
+ line-height: 17px;
2211
+ }
2212
+ .dshcf-settings-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding-bottom: 8px; }
2213
+ .dshcf-settings-readOnly { color: var(--dsw-alias-label-tertiary); margin: 12px 0 0; font-size: 12px; line-height: 1.5; }
2214
+ .dshcf-settings-field { flex-direction: column; gap: 6px; padding: 12px 0; display: flex; }
2215
+ .dshcf-settings-fieldHead { align-items: center; gap: 8px; display: flex; }
2216
+ .dshcf-settings-fieldLabel { min-width: 0; color: var(--dsw-alias-label-primary); flex: 1; font-size: 13px; font-weight: 500; line-height: 1.5; }
2217
+ .dshcf-settings-badges { align-items: center; gap: 8px; display: inline-flex; }
2218
+ .dshcf-settings-badge {
2219
+ white-space: nowrap;
2220
+ background: var(--dsw-alias-bg-module-platform);
2221
+ color: var(--dsw-alias-label-secondary);
2222
+ border-radius: 999px;
2223
+ padding: 1px 8px;
2224
+ font-size: 11px;
2225
+ font-weight: 500;
2226
+ line-height: 17px;
2227
+ }
2228
+ .dshcf-settings-reset { font: inherit; color: var(--dsw-alias-label-secondary); cursor: pointer; background: 0 0; border: none; padding: 0; font-size: 12px; line-height: 1.5; }
2229
+ .dshcf-settings-reset:hover:not(:disabled) { color: var(--dsw-alias-label-primary); }
2230
+ .dshcf-settings-reset:disabled { cursor: default; }
2231
+ .dshcf-settings-input {
2232
+ border: 1px solid var(--dsw-alias-border-l2);
2233
+ background: var(--dsw-alias-bg-layer-3);
2234
+ height: 34px;
2235
+ font: inherit;
2236
+ color: var(--dsw-alias-label-primary);
2237
+ border-radius: 8px;
2238
+ padding: 0 12px;
2239
+ font-size: 13px;
2240
+ line-height: 1.5;
2241
+ box-sizing: border-box;
2242
+ width: 100%;
2243
+ }
2244
+ .dshcf-settings-input:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
2245
+ .dshcf-settings-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
2246
+ .dshcf-settings-hint { color: var(--dsw-alias-label-tertiary); margin: 0; font-size: 12px; line-height: 1.5; }
2247
+ .dshcf-settings-footer { border-top: 1px solid var(--dsw-alias-border-l2); justify-content: flex-end; align-items: center; gap: 8px; padding: 12px 0 4px; display: flex; }
2248
+ .dshcf-settings-failed { min-width: 0; color: var(--dsw-alias-label-error); flex: 1; margin: 0; font-size: 12px; line-height: 1.5; }
2249
+ .dshcf-settings-discard,
2250
+ .dshcf-settings-save { appearance: none; font: inherit; cursor: pointer; border: 1px solid #0000; border-radius: 8px; padding: 5px 14px; font-size: 13px; line-height: 1.5; }
2251
+ .dshcf-settings-discard { border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); background: 0 0; }
2252
+ .dshcf-settings-discard:hover:not(:disabled) { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
2253
+ .dshcf-settings-save { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); }
2254
+ .dshcf-settings-discard:disabled,
2255
+ .dshcf-settings-save:disabled { opacity: .4; cursor: default; }
2256
+ .dshcf-settings-discard:focus-visible,
2257
+ .dshcf-settings-save:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
2258
+ `;
2259
+ var STYLE_ID2 = "dshcf-settings-style";
2260
+ function injectCardStyle() {
2261
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID2) !== null) return;
2262
+ const style = document.createElement("style");
2263
+ style.id = STYLE_ID2;
2264
+ style.textContent = CARD_CSS;
2265
+ document.head.appendChild(style);
2266
+ }
2267
+ function ChevronIcon(open) {
2268
+ const React = __require("react");
2269
+ const className = open ? "dshcf-settings-chevron dshcf-settings-chevronOpen" : "dshcf-settings-chevron";
2270
+ return React.createElement(
2271
+ "svg",
2272
+ { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, className },
2273
+ React.createElement("path", {
2274
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
2275
+ fill: "currentColor"
2276
+ })
2277
+ );
2278
+ }
2279
+ function StatusTextCard(props) {
2280
+ const React = __require("react");
2281
+ const scope = props.scope;
2282
+ const [open, setOpen] = React.useState(false);
2283
+ const [snapshot, setSnapshot] = React.useState(scope.getSnapshot());
2284
+ const [pending, setPending] = React.useState(null);
2285
+ const [saving, setSaving] = React.useState(false);
2286
+ const [failed, setFailed] = React.useState(false);
2287
+ React.useEffect(() => scope.subscribe(() => setSnapshot(scope.getSnapshot())), [scope]);
2288
+ if (snapshot.status !== "ready") return null;
2289
+ const value = snapshot.value;
2290
+ const base = snapshot.base;
2291
+ const user = snapshot.user;
2292
+ const currentText = value?.statusText ?? "";
2293
+ const defaultText = base?.statusText ?? DEFAULT_STATUS_TEXT2;
2294
+ const text = pending ? pending.text : currentText;
2295
+ const userHas = user !== void 0 && Object.prototype.hasOwnProperty.call(user, "statusText");
2296
+ const overridden = pending ? !pending.reset : userHas;
2297
+ const dirty = pending !== null && (pending.reset ? userHas : pending.text.trim() !== currentText);
2298
+ const writable = snapshot.writable;
2299
+ const discard = () => {
2300
+ setPending(null);
2301
+ setFailed(false);
2302
+ };
2303
+ const resetField = () => {
2304
+ setPending({ text: defaultText, reset: true });
2305
+ setFailed(false);
2306
+ };
2307
+ const edit = (next) => {
2308
+ setPending({ text: next, reset: false });
2309
+ setFailed(false);
2310
+ };
2311
+ const save = async () => {
2312
+ if (pending === null) return;
2313
+ setSaving(true);
2314
+ setFailed(false);
2315
+ try {
2316
+ if (pending.reset) await scope.unset("statusText");
2317
+ else await scope.set("statusText", pending.text.trim());
2318
+ setPending(null);
2319
+ } catch {
2320
+ setFailed(true);
2321
+ } finally {
2322
+ setSaving(false);
2323
+ }
2324
+ };
2325
+ const blocked = !dirty || saving;
2326
+ const cardClass = `dshcf-settings-card${open ? " dshcf-settings-cardOpen" : ""}`;
2327
+ return React.createElement("li", { className: cardClass }, [
2328
+ React.createElement(
2329
+ "button",
2330
+ {
2331
+ type: "button",
2332
+ className: "dshcf-settings-header",
2333
+ "aria-expanded": open,
2334
+ "aria-label": `${open ? "\u6536\u8D77\u8BBE\u7F6E" : "\u5C55\u5F00\u8BBE\u7F6E"}: \u72B6\u6001\u63D0\u793A\u8BCD`,
2335
+ onClick: () => setOpen(!open)
2336
+ },
2337
+ [
2338
+ React.createElement("span", { className: "dshcf-settings-headText" }, [
2339
+ 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")
2341
+ ]),
2342
+ dirty ? React.createElement("span", { className: "dshcf-settings-pending" }, "\u672A\u4FDD\u5B58") : null,
2343
+ ChevronIcon(open)
2344
+ ]
2345
+ ),
2346
+ open ? React.createElement("div", { className: "dshcf-settings-body" }, [
2347
+ !writable ? React.createElement("p", { className: "dshcf-settings-readOnly", role: "status" }, "\u672C\u90E8\u7F72\u7684\u8BBE\u7F6E\u4E3A\u53EA\u8BFB\u3002") : null,
2348
+ React.createElement("div", { className: "dshcf-settings-field" }, [
2349
+ React.createElement("div", { className: "dshcf-settings-fieldHead" }, [
2350
+ React.createElement("label", { className: "dshcf-settings-fieldLabel", htmlFor: "dshcf-status-text" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD"),
2351
+ overridden ? React.createElement("span", { className: "dshcf-settings-badges" }, [
2352
+ React.createElement("span", { className: "dshcf-settings-badge" }, "\u5DF2\u8986\u76D6"),
2353
+ React.createElement("button", { type: "button", className: "dshcf-settings-reset", disabled: !writable, onClick: resetField }, "\u6062\u590D\u9ED8\u8BA4")
2354
+ ]) : null
2355
+ ]),
2356
+ React.createElement("input", {
2357
+ id: "dshcf-status-text",
2358
+ className: "dshcf-settings-input",
2359
+ type: "text",
2360
+ value: text,
2361
+ placeholder: "Deep diving...",
2362
+ disabled: !writable,
2363
+ onChange: (event) => edit(event.target.value)
2364
+ }),
2365
+ React.createElement("p", { className: "dshcf-settings-hint" }, "\u4E3A\u7A7A\u65F6\u6062\u590D\u9ED8\u8BA4Deep diving...\u63D0\u793A\u8BCD\u72B6\u6001")
2366
+ ]),
2367
+ React.createElement("div", { className: "dshcf-settings-footer" }, [
2368
+ 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,
2369
+ React.createElement("button", { type: "button", className: "dshcf-settings-discard", disabled: !dirty || saving, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
2370
+ React.createElement("button", { type: "button", className: "dshcf-settings-save", disabled: blocked, onClick: save }, saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58")
2371
+ ])
2372
+ ]) : null
2373
+ ]);
2374
+ }
2375
+ 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
+ ));
2385
+ }
2386
+
1428
2387
  // src/client.ts
1429
2388
  var name = "dsh-auto-collapse";
1430
2389
  var inject = [];
1431
2390
  function apply(ctx) {
1432
2391
  ctx.effect(() => {
1433
- const controller = new FoldController();
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));
1434
2396
  controller.start();
1435
- return () => controller.stop();
1436
- }, "dsh-auto-collapse: fold observer");
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");
1437
2405
  }
1438
2406
  return __toCommonJS(client_exports);
1439
2407
  })();