dsh-auto-collapse 0.1.7 → 0.2.1

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
@@ -33,2399 +33,909 @@ var __dshcfBundle = (() => {
33
33
  name: () => name
34
34
  });
35
35
 
36
- // src/fold.ts
37
- var STYLE_ID = "dshcf-style";
38
- var DEFAULT_STATUS_TEXT = "Deep sleeping...";
39
- var TURN_STATUS_COPY_RE = /Deep diving[.…]*|深度求索中\s*[.…]*/;
40
- var ANIM_DURATION_MS = 180;
41
- var ANIM_EASING = "ease-out";
42
- var DISPLAY_OWNED_PROP = "--dshcf-display-owned";
43
- var AUDIT_TICK_MS = 1e3;
44
- var STICK_BOTTOM_THRESHOLD_PX = 24;
45
- var TOOL_LABELS = {
46
- bash: "Bash",
47
- pwsh: "Pwsh",
48
- read: "Read",
49
- web_fetch: "Read",
50
- web_search: "Search",
51
- grep: "Search",
52
- glob: "Search",
53
- write: "Write",
54
- edit: "Edit",
55
- run_code: "Code",
56
- cordis_package_inspect: "Inspect",
57
- cordis_runtime_inspect: "Inspect",
58
- cordis_run: "Run",
59
- cordis_stop: "Stop",
60
- cordis_undefine: "Remove"
61
- };
62
- var TURN_PROCESS_KIND = "turn-process";
63
- var NATIVE_OPEN_ATTR = "data-open";
64
- var CHIP_CSS = `
65
- .dshcf-chip {
66
- box-sizing: border-box;
67
- display: flex;
68
- align-self: flex-start;
69
- align-items: center;
70
- gap: 6px;
71
- width: fit-content;
72
- max-width: 100%;
73
- min-height: 24px;
74
- /* chip \u63D2\u5728\u5757\u5BBF\u4E3B\uFF08flowItem\uFF09\u5185\uFF0C\u4EAB\u53D7\u4E0D\u5230\u884C\u7684 row-gap 16px\uFF1B
75
- \u5C55\u5F00\u6001\u8865 margin-bottom \u5BF9\u9F50\u884C\u95F4\u8282\u594F\uFF1B\u6536\u8D77\u6001\u884C\u5DF2\u9690\u85CF\uFF0C\u82E5\u4ECD\u8865
76
- margin \u4F1A\u4E0E\u5757\u95F4 gap \u53E0\u52A0\u6210 32px\uFF0C\u6240\u4EE5\u6536\u8D77\u6001\u4E3A 0\u3002 */
77
- margin-bottom: 0;
78
- padding: 0;
79
- border: none;
80
- background: transparent;
81
- color: var(--dsw-alias-label-primary);
82
- font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
83
- text-align: left;
84
- cursor: pointer;
85
- user-select: none;
86
- /* \u5C55\u5F00\u6001\u8865\u7684 margin-bottom 16px \u7531 aria-expanded/has-body \u7FFB\u8F6C\u9A71\u52A8\uFF0C
87
- \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
88
- \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
89
- \u540E\u5F52\u96F6\uFF0C\u89C1 reconcileBlock / hasPendingCollapse\uFF09\u3002\u4E0D\u8BBE CSS transition
90
- \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
91
- \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 */
92
- }
93
- .dshcf-chip[aria-expanded="true"],
94
- .dshcf-chip.dshcf-has-body {
95
- margin-bottom: 16px;
96
- }
97
- /* context \u7B49 before-mounted chip \u662F flow \u7684\u76F4\u63A5\u5B50\u9879\uFF0C\u5DF2\u7ECF\u4EAB\u53D7\u5BBF\u4E3B
98
- row-gap: 16px\uFF1B\u5C55\u5F00\u65F6\u4E0D\u80FD\u518D\u53E0\u52A0\u81EA\u8EAB margin\uFF0C\u5426\u5219\u4E8C\u7EA7\u5230\u4E09\u7EA7\u4F1A\u53D8 32px\u3002 */
99
- .dshcf-chip.dshcf-flow-chip {
100
- margin-bottom: 0;
101
- }
102
- .dshcf-chip:hover {
103
- background: transparent;
104
- }
105
-
106
- /* leading\uFF1A\u56FA\u5B9A 14x14\uFF08\u601D\u8003\u5757 = \u539F\u751F think \u56FE\u6807\uFF1B\u5DE5\u5177\u5757 = \u539F\u751F command
107
- \u56FE\u6807 IconApiOutline14\uFF0C\u514B\u9686\u81EA\u771F\u5B9E GenericCommandCard leading\uFF0C\u627E\u4E0D\u5230\u65F6
108
- \u9000\u56DE\u7EC8\u7AEF\u5C0F\u65B9\u5757\uFF09\uFF0C\u884C\u9AD8 24px \u4E0E\u539F\u751F\u884C\u5BF9\u9F50\uFF1B\u8FD0\u884C\u4E2D\u8DF3\u52A8\u3002svg \u5C3A\u5BF8\u7531\u5404\u81EA
109
- width/height \u5C5E\u6027\u51B3\u5B9A\uFF08command 14x14\u3001think 14x14\u3001\u7EC8\u7AEF 12x10 \u515C\u5E95\uFF09\uFF0C
110
- \u4E0D\u5728\u6B64\u5904\u5F3A\u5236\u3002 */
111
- .dshcf-chip .dshcf-leading {
112
- flex: none;
113
- display: flex;
114
- align-items: center;
115
- justify-content: center;
116
- width: 16px;
117
- height: 16px;
118
- }
119
- .dshcf-chip .dshcf-leading svg {
120
- display: block;
121
- color: var(--dsw-alias-label-tertiary);
122
- }
123
- .dshcf-chip.running .dshcf-leading svg {
124
- /* \u8FD0\u884C\u8272\u4FDD\u7559\uFF1B\u56FE\u6807\u8DF3\u52A8\u52A8\u753B\u5DF2\u6309\u7528\u6237\u8981\u6C42\u79FB\u9664\u3002 */
125
- color: var(--dsw-static-deepseek-500, #4d6bfe);
126
- }
127
-
128
- /* \u8FD0\u884C\u6307\u793A\u4E09\u4E2A\u70B9\uFF1A\u5DF2\u6309\u7528\u6237\u8981\u6C42\u79FB\u9664\uFF08\u4E0D\u518D\u521B\u5EFA/\u663E\u793A\uFF09\u3002 */
129
-
130
- /* \u51FA\u9519\u7EA2 / \u4E2D\u65AD\u7425\u73C0\uFF08\u9759\u6B62\u6001\uFF09\u3002 */
131
- .dshcf-chip.error:not(.running) .dshcf-leading svg {
132
- color: var(--dsw-alias-state-error-primary, #e5484d);
133
- }
134
- .dshcf-chip.stopped:not(.running) .dshcf-leading svg {
135
- color: var(--dsw-alias-state-warning-primary, #f5a524);
136
- }
137
-
138
- .dshcf-chip .dshcf-chip-title {
139
- flex: none;
140
- font-weight: 400;
141
- max-width: 70%;
142
- overflow: hidden;
143
- text-overflow: ellipsis;
144
- white-space: nowrap;
145
- }
146
- .dshcf-chip .dshcf-chip-sep {
147
- flex: none;
148
- width: 2px;
149
- height: 2px;
150
- border-radius: 1px;
151
- background: var(--dsw-alias-label-caption, rgba(127, 127, 127, 0.5));
152
- }
153
- /* \u6458\u8981\u4E0D\u6491\u6EE1\uFF08flex 0 1\uFF09\uFF0C\u8BA9 chevron \u7D27\u8DDF\u5728\u6587\u672C\u53F3\u65B9\u800C\u975E\u884C\u5C3E\u3002 */
154
- .dshcf-chip .dshcf-chip-summary {
155
- flex: 0 1 auto;
156
- min-width: 0;
157
- max-width: 100%;
158
- overflow: hidden;
159
- text-overflow: ellipsis;
160
- white-space: nowrap;
161
- }
162
- /* running \u6458\u8981\uFF1A\u8DDF\u968F\u6EDA\u52A8\u663E\u793A\u6700\u65B0\u5185\u5BB9\uFF08text-overflow: clip\uFF0C\u539F\u751F\u540C\u6B3E\uFF09\u3002 */
163
- .dshcf-chip.running .dshcf-chip-summary {
164
- text-overflow: clip;
165
- }
166
- /* \u6298\u53E0\u884C\u6587\u5B57\uFF1A\u590D\u7528 DSH \u539F\u751F label token\uFF08\u5DE5\u5177\u884C\u540C\u6E90\uFF09\uFF0C\u533A\u522B\u4E8E\u6B63\u6587\u7EAF\u767D\u3002 */
167
- .dshcf-chip .dshcf-chip-title {
168
- color: var(--dsw-alias-label-primary);
169
- }
170
- .dshcf-chip .dshcf-chip-summary {
171
- color: var(--dsw-alias-label-tertiary);
172
- }
173
- /* \u5DE5\u5177\u884C\u6458\u8981\uFF08\u547D\u4EE4/\u8DEF\u5F84\uFF09\u7B49\u5BBD\u5B57\u4F53 + \u4EE3\u7801\u886C\u5E95\uFF08\u7D20\u6750 Codex \u540C\u6B3E\uFF09\u3002
174
- \u884C\u9AD8\u4E0E chip \u4E00\u81F4\uFF0824px\uFF09\uFF0C\u6D41\u5F0F\u66F4\u65B0\u65F6\u6458\u8981\u5355\u884C ellipsis \u4E0D\u6362\u884C\u4E0D\u6491\u9AD8\u3002 */
175
- .dshcf-chip[data-kind="tool"] .dshcf-chip-summary {
176
- font-family: ui-monospace, SFMono-Regular, Consolas, "Courier New", monospace;
177
- font-size: 12px;
178
- line-height: 24px;
179
- background: rgba(127, 127, 127, 0.14);
180
- border-radius: 4px;
181
- padding: 0 6px;
182
- }
183
- .dshcf-chip[data-kind="tool"] .dshcf-chip-summary:empty {
184
- background: none;
185
- padding: 0;
186
- }
187
-
188
- /* \u8FD0\u884C\u4E2D\u6587\u5B57\u4F7F\u7528\u5E73\u6ED1\u547C\u5438\u52A8\u753B\uFF08Pulse\uFF09\uFF0C\u9002\u914D\u6D45\u8272/\u6DF1\u8272\u4E3B\u9898\uFF0C\u907F\u514D background-clip \u88C1\u5207\u95EE\u9898\u3002 */
189
- .dshcf-chip.running .dshcf-chip-title,
190
- .dshcf-chip.running .dshcf-chip-summary {
191
- color: var(--dsw-alias-label-tertiary, #8b8f99);
192
- -webkit-text-fill-color: currentColor;
193
- animation: dshcf-pulse 1.6s ease-in-out infinite;
194
- }
195
- .dshcf-chip.running[data-kind="tool"] .dshcf-chip-summary {
196
- background: transparent;
197
- }
198
- @keyframes dshcf-pulse {
199
- 0%, 100% { opacity: 1; }
200
- 50% { opacity: 0.5; }
201
- }
202
-
203
- /* "\u5DF2\u5904\u7406"\u884C\uFF1A\u6700\u7EC8\u8F93\u51FA\u51FA\u73B0\u540E\u5DE5\u4F5C\u8FC7\u7A0B\u6574\u4F53\u9690\u85CF\uFF0C\u53EA\u7559\u8FD9\u4E00\u884C + \u65F6\u957F\u3002
204
- \u5B57\u4F53\u4E0E\u4E8C\u7EA7 chip \u5BF9\u9F50\uFF0814px/24px\uFF09\uFF0C\u5DE6\u53F3\u65E0\u5185\u8FB9\u8DDD\uFF08\u4E0E\u6B63\u6587\u5DE6\u7F18\u5BF9\u9F50\uFF09\u3002 */
205
- .dshcf-processed {
206
- display: inline-flex;
207
- align-self: flex-start;
208
- width: fit-content;
209
- max-width: 100%;
210
- align-items: center;
211
- gap: 6px;
212
- padding: 2px 0;
213
- border: none;
214
- background: none;
215
- font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
216
- /* \u5BF9\u9F50 DSH \u539F\u751F\u5DE5\u5177\u884C\u6458\u8981\u7684\u6B21\u7EA7\u5C42\u7EA7\uFF08label-tertiary\uFF09\u3002 */
217
- color: var(--dsw-alias-label-tertiary);
218
- cursor: pointer;
219
- user-select: none;
220
- border-radius: 4px;
221
- transition: color 0.15s ease;
222
- }
223
- .dshcf-processed:hover {
224
- color: var(--dsw-alias-label-primary);
225
- background: transparent;
226
- }
227
- .dshcf-processed:focus-visible {
228
- outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
229
- outline-offset: 2px;
230
- }
231
- /* \u6298\u53E0\u7BAD\u5934\uFF1A\u4F7F\u7528 DSH \u539F\u751F IconChevronDownOutline14 \u7684 14x14 path\u3002 */
232
- .dshcf-processed .dshcf-processed-chevron {
233
- display: inline-flex;
234
- flex: none;
235
- width: 14px;
236
- height: 14px;
237
- color: var(--dsw-alias-label-tertiary);
238
- opacity: 0.55;
239
- transform: rotate(-90deg);
240
- transition: transform 0.12s ease, opacity 0.1s ease;
241
- }
242
- .dshcf-processed:hover .dshcf-processed-chevron {
243
- opacity: 0.9;
244
- }
245
- .dshcf-processed[aria-expanded="true"] .dshcf-processed-chevron {
246
- transform: rotate(0deg);
247
- }
248
-
249
- /* \u4E09\u7EA7\u5408\u5E76\u601D\u8003\u884C\uFF1A\u5C55\u5F00\u4E8C\u7EA7\u540E\u8FDE\u7EED\u601D\u8003\u5408\u5E76\u4E3A\u4E00\u884C\uFF08\u6807\u9898 = \u7B2C\u4E00\u884C\u601D\u8003\u5185\u5BB9\uFF09\u3002
250
- \u6837\u5F0F\u4E0E chip \u540C\u65CF\uFF0816px \u56FE\u6807\u76D2\u300114px/24px\u3001\u539F\u751F label token \u8272\uFF09\u3002 */
251
- .dshcf-merged-think {
252
- box-sizing: border-box;
253
- display: flex;
254
- align-self: flex-start;
255
- align-items: center;
256
- gap: 6px;
257
- width: fit-content;
258
- max-width: 100%;
259
- min-height: 24px;
260
- margin: 0;
261
- padding: 0;
262
- border: none;
263
- background: transparent;
264
- color: var(--dsw-alias-label-primary);
265
- font: 400 14px/24px system-ui, -apple-system, "Segoe UI", sans-serif;
266
- text-align: left;
267
- cursor: pointer;
268
- user-select: none;
269
- }
270
- .dshcf-merged-think .dshcf-leading {
271
- flex: none;
272
- display: flex;
273
- align-items: center;
274
- justify-content: center;
275
- width: 16px;
276
- height: 16px;
277
- }
278
- .dshcf-merged-think .dshcf-leading svg {
279
- display: block;
280
- color: var(--dsw-alias-label-tertiary);
281
- }
282
- .dshcf-merged-think .dshcf-merged-title {
283
- flex: 0 1 auto;
284
- min-width: 0;
285
- max-width: 85%;
286
- overflow: hidden;
287
- text-overflow: ellipsis;
288
- white-space: nowrap;
289
- color: var(--dsw-alias-label-secondary);
290
- font-weight: 400;
291
- }
292
- .dshcf-merged-think .dshcf-chevron {
293
- flex: none;
294
- width: 14px;
295
- height: 14px;
296
- color: var(--dsw-alias-label-secondary);
297
- opacity: 0.55;
298
- transform: rotate(-90deg);
299
- transition: transform 0.12s ease, opacity 0.1s ease;
300
- }
301
- .dshcf-merged-think:hover .dshcf-chevron,
302
- .dshcf-merged-think:focus-visible .dshcf-chevron {
303
- opacity: 0.9;
304
- }
305
- .dshcf-merged-think[aria-expanded="true"] .dshcf-chevron {
306
- transform: rotate(0deg);
307
- }
308
- /* \u5408\u5E76\u601D\u8003\u5185\u5BB9\u5757\uFF1A\u56DB\u4E2A\u601D\u8003\u5408\u5E76\u4E3A\u4E00\u4E2A\u6574\u4F53\uFF08\u5BF9\u9F50\u56FE\u6807\u53F3\u4FA7\u7F29\u8FDB\uFF09\u3002 */
309
- .dshcf-merged-body {
310
- margin: 0 0 16px;
311
- padding-left: 22px;
312
- color: var(--dsw-alias-label-secondary);
313
- font: 400 13px/22px system-ui, -apple-system, "Segoe UI", sans-serif;
314
- white-space: pre-wrap;
315
- word-break: break-word;
316
- }
317
-
318
- /* chevron\uFF1A\u9ED8\u8BA4\u9690\u85CF\uFF0Chover/focus \u6D6E\u73B0\uFF0C\u5C55\u5F00\u65F6\u65CB\u8F6C 90\xB0\uFF08Codex \u540C\u6B3E\uFF09\u3002 */
319
- .dshcf-chip .dshcf-chevron {
320
- flex: none;
321
- width: 14px;
322
- height: 14px;
323
- color: var(--dsw-alias-label-tertiary);
324
- opacity: 0.5;
325
- transform: rotate(-90deg);
326
- transition: opacity 0.1s ease, transform 0.12s ease;
327
- }
328
- .dshcf-chip:hover .dshcf-chevron,
329
- .dshcf-chip:focus-visible .dshcf-chevron,
330
- .dshcf-chip[aria-expanded="true"] .dshcf-chevron {
331
- opacity: 0.9;
332
- }
333
- .dshcf-chip[aria-expanded="true"] .dshcf-chevron {
334
- transform: rotate(0deg);
335
- }
336
- .dshcf-chip:focus-visible {
337
- outline: 2px solid var(--dsw-alias-state-focus-ring, rgba(77, 107, 254, 0.8));
338
- outline-offset: 2px;
339
- border-radius: 4px;
340
- }
341
-
342
- @media (prefers-reduced-motion: reduce) {
343
- .dshcf-chip.running .dshcf-leading svg { animation: none; }
344
- .dshcf-chip.running .dshcf-chip-title,
345
- .dshcf-chip.running .dshcf-chip-summary {
346
- animation: none;
347
- opacity: 1;
348
- }
349
- }
350
- `;
351
- var FoldController = class {
352
- constructor(statusTextProvider2, options) {
353
- this.observer = null;
354
- /** 防止同一个控制器重复注册 observer、定时器和可见性监听。 */
355
- this.started = false;
356
- /** body 尚未创建时等待 DOMContentLoaded,再补一次启动。 */
357
- this.waitingForBody = false;
358
- this.onDomContentLoaded = () => {
359
- this.waitingForBody = false;
360
- this.start();
361
- };
362
- this.raf = 0;
363
- this.timer = 0;
364
- this.disposed = false;
365
- this.lastPassError = "";
366
- this.flow = null;
367
- /** 稳定 block key → 当前 React 渲染中的 chip/host。 */
368
- this.chips = /* @__PURE__ */ new Map();
369
- this.currentBlocks = /* @__PURE__ */ new Map();
370
- this.blockExpanded = /* @__PURE__ */ new Map();
371
- /** host → 三级合并思考行(展开二级后连续思考合并显示为一个三级行)。 */
372
- this.mergedThinks = /* @__PURE__ */ new Map();
373
- /** 合并思考行的展开状态(true = 显示合并内容块)。 */
374
- this.mergedExpanded = /* @__PURE__ */ new WeakSet();
375
- /** 合并内容缓存(首次从原生行读取后保存,pass 重建内容块时不再重新展开原生行)。 */
376
- this.mergedBodyTexts = /* @__PURE__ */ new WeakMap();
377
- /** 合并行标题缓存(原生行展开态提取不到摘要时保持首次标题,不丢成“思考”)。 */
378
- this.mergedTitles = /* @__PURE__ */ new WeakMap();
379
- /** 稳定 segment key → 一级折叠行与展开状态。 */
380
- this.segmentStates = /* @__PURE__ */ new Map();
381
- /** segment 首次观察到 running 的时间,用于没有官方时长的实时回合。 */
382
- this.runningSince = /* @__PURE__ */ new Map();
383
- /** 曾完成过的 segment key:段恢复运行时据此重开本地计时,防止重新结算
384
- * 的本地时长吞掉完成间隙。 */
385
- this.completedOnce = /* @__PURE__ */ new Set();
386
- /** 插件改写 display 前的精确原值;受控集合用于分类漂移和 stop() 恢复。 */
387
- this.originalDisplay = /* @__PURE__ */ new WeakMap();
388
- this.controlledDisplay = /* @__PURE__ */ new Set();
389
- /** 元素 → 插件最后确保的 display 值:恢复前与当前内联值比对,漂移即视为
390
- * 外部介入(镜像 turnStatusTexts 的 original/written 双快照守卫,issue #11)。 */
391
- this.writtenDisplay = /* @__PURE__ */ new WeakMap();
392
- /** 被改写为状态提示词的原生状态文本:original = 宿主原文(卸载还原用),
393
- * written = 插件最后一次写入的值(仅当节点仍等于它时才还原,避免覆盖
394
- * 宿主在插件写入之后的状态更新)。 */
395
- this.turnStatusTexts = /* @__PURE__ */ new Map();
396
- /** 正文判定缓存(消息元素 → 有无正文):流式期间只有被 mutation 命中的
397
- * 消息失效重算,历史消息跨 pass 复用,避免每帧全量 TreeWalker。 */
398
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
399
- /** 自上次 pass 以来子树发生变化的 flow 顶层消息;pass 开头统一失效。 */
400
- this.dirtyMessages = /* @__PURE__ */ new Set();
401
- /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
402
- /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
403
- * 用 Map 不用 WeakMap——switchFlow/stop 需要遍历全量 cancel。 */
404
- this.pendingAnims = /* @__PURE__ */ new Map();
405
- /** 手势点击的一次性可动画 block key;segment 级点击另保留中间正文的门控。 */
406
- this.animatableKeys = /* @__PURE__ */ new Set();
407
- /** segment 点击时只让点击前已存在的 block 播放 reveal;流式中新出现的
408
- * 临时分裂块直接显示,避免分类收敛时留下半透明 stale chip。 */
409
- this.animatableSegmentBlocks = /* @__PURE__ */ new Map();
410
- /** 外部变更对账定时器句柄(自重排 setTimeout 链,见 armAuditLoop)。 */
411
- this.auditTimer = 0;
412
- /** 上一轮 pass 记录的关键元素内联 display,用于 audit 轻量检测漂移。
413
- * audit 只读这份快照,不在页面稳定时重新执行完整 pass。 */
414
- this.auditDisplays = /* @__PURE__ */ new Map();
415
- /** 自上次 pass 以来 flow 子树发生过结构变化(childList)或正文判定翻转:
416
- * 为 true 时 pass 重建分块快照,否则复用 currentBlocks——characterData/
417
- * attributes 批次(流式文本、data-state 翻转)不改变块结构,跳过全量
418
- * querySelectorAll 重扫(issue #14:长会话下每轮重扫造成主线程卡顿)。 */
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;
434
- /** 滚动稳定化(issue #14):flow 最近的滚动容器缓存(按 flow 身份失效)。 */
435
- this.scrollContainer = null;
436
- this.scrollContainerFlow = null;
437
- /** 回到前台立即补一轮对账;后台 tab 由 document.hidden 门控跳过。 */
438
- this.onVisibilityChange = () => {
439
- if (typeof document === "undefined" || document.hidden !== true) this.schedule();
440
- };
441
- this.statusTextProvider = statusTextProvider2 ?? (() => DEFAULT_STATUS_TEXT);
442
- this.auditIntervalMs = options?.auditIntervalMs ?? AUDIT_TICK_MS;
443
- }
444
- /** 设置变更后重跑一轮,让状态提示词立即生效。 */
445
- refresh() {
446
- this.schedule();
447
- }
448
- start() {
449
- if (this.disposed || this.started || this.waitingForBody) return;
450
- if (typeof document === "undefined") return;
451
- if (document.body === null) {
452
- if (typeof document.addEventListener !== "function") return;
453
- this.waitingForBody = true;
454
- document.addEventListener("DOMContentLoaded", this.onDomContentLoaded, { once: true });
455
- return;
456
- }
457
- injectStyle();
458
- try {
459
- this.observer = new MutationObserver((records) => {
460
- if (this.shouldSchedule(records)) {
461
- this.markDirty(records);
462
- this.schedule();
463
- }
464
- });
465
- this.observer.observe(document.body, {
466
- childList: true,
467
- subtree: true,
468
- attributes: true,
469
- // 原生 TurnProcess 开合只改 attribute(成员 data-turn-process-hidden、
470
- // 摘要按钮 data-open),必须进过滤器才能即时调度;插件自身从不写
471
- // 这两个属性,不会自激。
472
- attributeFilter: ["data-selected", "data-state", "data-turn-process-hidden", "data-open"],
473
- // 流式文本更新(React 改 text node 的 data)属于 characterData
474
- // mutation:不观察则二级摘要/滚动跟随只能靠偶发结构变化驱动,
475
- // 变成“隔几秒跳一次”。所有文本写入都有守卫(值不变不写),
476
- // 不会自激。
477
- characterData: true
478
- });
479
- this.started = true;
480
- this.armAuditLoop();
481
- this.schedule();
482
- } catch (error) {
483
- this.observer?.disconnect();
484
- this.observer = null;
485
- this.started = false;
486
- this.reportError(error);
487
- throw error;
488
- }
489
- }
490
- /** 外部显示变更对账循环(issue #11 Bug B):外部对宿主行的 style 写入不产生
491
- * observer record(style 不在 attributeFilter 内,监听会因插件自身直写 style
492
- * 自激),改用低频轻量对账兜底——发现漂移后才由 pass 收敛;
493
- * 后台 tab 由 document.hidden 门控跳过,回前台由 visibilitychange 立即补一轮。
494
- * 用自重排 setTimeout 链而非 setInterval:与 schedule 的兜底定时器同源,
495
- * 测试桩 clearTimers 后链条自然熄灭。 */
496
- armAuditLoop() {
497
- if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
498
- document.addEventListener("visibilitychange", this.onVisibilityChange);
499
- }
500
- this.rearmAudit();
501
- }
502
- rearmAudit() {
503
- if (this.disposed || this.auditTimer !== 0) return;
504
- this.auditTimer = setTimeout(() => {
505
- this.auditTimer = 0;
506
- if (this.disposed) return;
507
- if (typeof document !== "undefined" && document.hidden === true) {
508
- this.rearmAudit();
509
- return;
510
- }
511
- this.audit();
512
- this.rearmAudit();
513
- }, this.auditIntervalMs);
514
- }
515
- /** 低成本显示状态对账:只有发现外部漂移时才启动完整 pass。
516
- *
517
- * 外部 style.display 写入不会产生当前 observer 的 attribute 记录,
518
- * 因此仍保留 audit;但稳定页面不应每秒重扫整个 flow。快照只覆盖
519
- * flow 顶层行、插件控制中的宿主行和插件自有展示行,避免引入布局读取。 */
520
- audit() {
521
- if (this.disposed) return;
522
- const flow = this.flow;
523
- if (flow === null || !flow.isConnected) {
524
- if (flow !== null) this.schedule();
525
- return;
526
- }
527
- const current = this.collectAuditDisplays(flow);
528
- if (current.size !== this.auditDisplays.size) {
529
- this.schedule();
530
- return;
531
- }
532
- for (const [el, display] of current) {
533
- if (this.auditDisplays.get(el) !== display) {
534
- this.schedule();
535
- return;
536
- }
537
- if (this.controlledDisplay.has(el) && this.displayForeign(el)) {
538
- this.schedule();
539
- return;
540
- }
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
- }
553
- }
554
- stop() {
555
- this.disposed = true;
556
- this.started = false;
557
- this.waitingForBody = false;
558
- if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
559
- document.removeEventListener("DOMContentLoaded", this.onDomContentLoaded);
560
- }
561
- if (this.raf !== 0) cancelAnimationFrame(this.raf);
562
- if (this.timer !== 0) clearTimeout(this.timer);
563
- if (this.auditTimer !== 0) {
564
- clearTimeout(this.auditTimer);
565
- this.auditTimer = 0;
566
- }
567
- if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
568
- document.removeEventListener("visibilitychange", this.onVisibilityChange);
569
- }
570
- this.observer?.disconnect();
571
- this.switchFlow(null);
572
- removeStyle();
573
- }
574
- /** body 级 observer 只负责发现 flow 替换;已有 flow 外的文本变化不再触发全量扫描。 */
575
- shouldSchedule(records) {
576
- if (records.length === 0 || this.flow === null || !this.flow.isConnected) return true;
577
- return records.some((record) => this.isRelevantMutation(record));
578
- }
579
- /** 判断 mutation 是否会影响宿主 flow;插件自有节点的回写直接忽略,
580
- * 避免“pass 插入 chip → observer 再开一轮 pass”的自激循环。 */
581
- isRelevantMutation(record) {
582
- const flow = this.flow;
583
- if (flow === null || !flow.isConnected) return true;
584
- if (!nodeWithin(record.target, flow) && !nodeWithin(flow, record.target)) return false;
585
- if (record.type === "characterData" && record.target instanceof Text && this.turnStatusTexts.has(record.target)) {
586
- return false;
587
- }
588
- const changed = [
589
- ...Array.from(record.addedNodes ?? []),
590
- ...Array.from(record.removedNodes ?? [])
591
- ];
592
- if (changed.length > 0 && changed.every(isPluginOwnedNode)) return false;
593
- if (changed.length === 0 && isPluginOwnedNode(record.target)) return false;
594
- return true;
595
- }
596
- /** 记录本批 mutation 命中的 flow 顶层消息,供正文判定缓存定向失效。
597
- * 从 record.target 沿 parentNode 走到 flow 的直接子级即所属消息。
598
- * 失效粒度(issue #14):只有 childList 使分块快照失效;flow 直挂层的
599
- * 插件节点/文本节点与 flow 外的混批记录不影响任何消息的正文判定,跳过
600
- * 而非全量失效——旧逻辑把它们全部放大成 O(全会话) 的 TreeWalker 重扫,
601
- * 长会话流式期间每帧如此。空批次仍保守全量失效(测试桩的调度通知)。 */
602
- markDirty(records) {
603
- const flow = this.flow;
604
- if (flow === null || !flow.isConnected) return;
605
- if (records.length === 0) {
606
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
607
- this.dirtyMessages.clear();
608
- this.structureDirty = true;
609
- return;
610
- }
611
- for (const record of records.filter((record2) => this.isRelevantMutation(record2))) {
612
- if (record.type === "childList") this.structureDirty = true;
613
- if (record.target === flow) {
614
- for (const node of record.addedNodes ?? []) {
615
- if (node instanceof HTMLElement) this.dirtyMessages.add(node);
616
- }
617
- continue;
618
- }
619
- const owner = flowChildOwner(record.target, flow);
620
- if (owner !== null) this.dirtyMessages.add(owner);
621
- }
622
- }
623
- schedule() {
624
- if (this.disposed || this.raf !== 0) return;
625
- this.raf = requestAnimationFrame(() => {
626
- this.raf = 0;
627
- if (this.timer !== 0) {
628
- clearTimeout(this.timer);
629
- this.timer = 0;
630
- }
631
- this.runPass();
632
- });
633
- if (this.timer !== 0) clearTimeout(this.timer);
634
- this.timer = setTimeout(() => {
635
- this.timer = 0;
636
- if (this.raf !== 0) {
637
- cancelAnimationFrame(this.raf);
638
- this.raf = 0;
639
- this.runPass();
640
- }
641
- }, 60);
642
- }
643
- /** 异步 observer 异常不能静默杀死协调器;保留非可视诊断并允许后续 mutation 重试。 */
644
- runPass() {
645
- try {
646
- this.pass();
647
- this.lastPassError = "";
648
- const style = document.getElementById(STYLE_ID);
649
- style?.setAttribute("data-dshcf-state", "active");
650
- style?.removeAttribute("data-dshcf-error");
651
- } catch (error) {
652
- this.reportError(error);
653
- } finally {
654
- this.animatableKeys.clear();
655
- this.animatableSegmentBlocks.clear();
656
- }
657
- }
658
- reportError(error) {
659
- const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
660
- const style = document.getElementById(STYLE_ID);
661
- style?.setAttribute("data-dshcf-state", "error");
662
- style?.setAttribute("data-dshcf-error", message.slice(0, 500));
663
- if (message === this.lastPassError) return;
664
- this.lastPassError = message;
665
- console.error("[dsh-auto-collapse] fold pass failed", error);
666
- }
667
- /** 一轮重放:重算堆积 → 应用折叠/展开 → 摆放并更新 chip → 替换状态行。 */
668
- pass() {
669
- if (this.disposed) return;
670
- const nextFlow = findFlow();
671
- if (nextFlow !== this.flow) this.switchFlow(nextFlow);
672
- const flow = this.flow;
673
- if (flow === null) return;
674
- let bodyFlipped = false;
675
- for (const el of this.dirtyMessages) {
676
- const prev = this.bodyTextCache.get(el);
677
- this.bodyTextCache.delete(el);
678
- if (prev !== void 0 && this.hasBodyCached(el) !== prev) bodyFlipped = true;
679
- }
680
- this.dirtyMessages.clear();
681
- const rebuildBlocks = this.structureDirty || bodyFlipped || this.currentBlocks.size === 0;
682
- this.structureDirty = false;
683
- const blocks = rebuildBlocks ? findBlocks(flow, (el) => this.hasBodyCached(el)) : [...this.currentBlocks.values()];
684
- this.currentBlocks = new Map(blocks.map((block) => [block.key, block]));
685
- const segments = buildSegments(flow, blocks, (el) => this.hasBodyCached(el));
686
- const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
687
- this.nativeTurns = buildNativeTurnMap(flow);
688
- const scrollAnchor = this.captureScrollAnchor(flow);
689
- for (const segment of segments) {
690
- if (!segment.running) continue;
691
- if (this.completedOnce.has(segment.key)) {
692
- this.completedOnce.delete(segment.key);
693
- this.runningSince.delete(segment.key);
694
- }
695
- if (!this.runningSince.has(segment.key)) {
696
- this.runningSince.set(segment.key, Date.now());
697
- }
698
- }
699
- const completedKeys = /* @__PURE__ */ new Set();
700
- for (const snapshot of segments) {
701
- if (!snapshot.closed || snapshot.running || !snapshot.hasWork) continue;
702
- completedKeys.add(snapshot.key);
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
- }
710
- let state = this.segmentStates.get(snapshot.key);
711
- if (state === void 0) {
712
- state = { key: snapshot.key, row: null, expanded: false, snapshot };
713
- this.segmentStates.set(snapshot.key, state);
714
- } else {
715
- state.snapshot = snapshot;
716
- }
717
- const started = this.runningSince.get(snapshot.key);
718
- const parsed = snapshot.boundary === null ? void 0 : parseTurnDuration(snapshot.boundary);
719
- if (parsed !== void 0) state.duration = parsed;
720
- else if (state.duration === void 0 && started !== void 0) state.duration = Date.now() - started;
721
- if (state.row === null || !state.row.isConnected) state.row = this.createProcessedRow(state);
722
- this.syncProcessedRow(state);
723
- }
724
- for (const [key, state] of [...this.segmentStates]) {
725
- if (completedKeys.has(key)) continue;
726
- state.row?.remove();
727
- this.segmentStates.delete(key);
728
- }
729
- const segmentByBlock = /* @__PURE__ */ new Map();
730
- for (const segment of segments) {
731
- for (const block of segment.blocks) segmentByBlock.set(block.key, segment);
732
- }
733
- const desiredHidden = /* @__PURE__ */ new Set();
734
- const seenBlocks = /* @__PURE__ */ new Set();
735
- for (const block of blocks) {
736
- seenBlocks.add(block.key);
737
- this.reconcileBlock(block, segmentByBlock.get(block.key) ?? null, desiredHidden);
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;
754
- for (const segment of segments) {
755
- const state = this.segmentStates.get(segment.key);
756
- const collapse = state !== void 0 && !state.expanded;
757
- const animate = this.animatableKeys.has(segment.key);
758
- for (const middle of segment.middleSteps) {
759
- if (collapse) this.hideElement(middle, desiredHidden, animate);
760
- else this.restoreElement(middle, animate);
761
- }
762
- if (segment.finalStep !== null) this.restoreElement(segment.finalStep);
763
- }
764
- for (const segment of segments) {
765
- if (segment.hasWork && hasVisibleSegmentWork(segment)) continue;
766
- const state = this.segmentStates.get(segment.key);
767
- if (state !== void 0 && state.row !== null) {
768
- state.row.remove();
769
- state.row = null;
770
- }
771
- for (const block of segment.blocks) this.suppressBlock(block, desiredHidden);
772
- for (const middle of segment.middleSteps) this.retainDisplayControl(middle, desiredHidden);
773
- if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
774
- }
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
- }
781
- this.restoreUnusedDisplays(desiredHidden);
782
- for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
783
- this.stabilizeScrollAfterFold(scrollAnchor);
784
- for (const key of [...this.runningSince.keys()]) {
785
- if (!liveSegmentKeys.has(key)) this.runningSince.delete(key);
786
- }
787
- for (const key of [...this.completedOnce]) {
788
- if (!liveSegmentKeys.has(key)) this.completedOnce.delete(key);
789
- }
790
- for (const [node] of [...this.turnStatusTexts]) {
791
- if (!node.isConnected) this.turnStatusTexts.delete(node);
792
- }
793
- for (const [el] of [...this.pendingAnims]) {
794
- if (!el.isConnected) this.pendingAnims.delete(el);
795
- }
796
- const statusText = this.statusTextProvider();
797
- if (statusText === void 0 || statusText === "") {
798
- restoreTurnStatus(this.turnStatusTexts);
799
- } else {
800
- replaceTurnStatus(flow, this.turnStatusTexts, statusText);
801
- }
802
- this.captureAuditDisplays(flow);
803
- }
804
- /** flow 最近的滚动容器(issue #14 滚动稳定化的测量基准):沿父链找第一个
805
- * overflow-y 为 auto/scroll 且实际可滚动的祖先。结果按 flow 身份缓存;
806
- * 找不到时不缓存(内容增长后祖先可能变为可滚动),每 pass 重探的代价
807
- * 只是几次 clean-layout 的 computed style / scrollHeight 读取。 */
808
- findScrollContainer(flow) {
809
- if (this.scrollContainerFlow === flow && this.scrollContainer !== null && this.scrollContainer.isConnected && nodeWithin(flow, this.scrollContainer)) {
810
- return this.scrollContainer;
811
- }
812
- let node = flow.parentElement;
813
- while (node !== null) {
814
- if (typeof getComputedStyle === "function") {
815
- const oy = getComputedStyle(node).overflowY;
816
- if ((oy === "auto" || oy === "scroll") && node.scrollHeight > node.clientHeight) break;
817
- }
818
- node = node.parentElement;
819
- }
820
- if (node === null) return null;
821
- this.scrollContainer = node;
822
- this.scrollContainerFlow = flow;
823
- return node;
824
- }
825
- /** 几何写入前捕捉贴底意图(issue #14):视口距底 ≤ 上游 FOLLOW_THRESHOLD
826
- * 时返回锚点,stabilizeScrollAfterFold 在写入后同帧钉回底部。远离底部
827
- * (用户正在滚动浏览)时不干预——视口上方的高度变化由浏览器 scroll
828
- * anchoring 补偿,视口下方的折叠不可见,插件再写 scrollTop 只会加入
829
- * 上游吸底回写的拉锯。 */
830
- captureScrollAnchor(flow) {
831
- const scroller = this.findScrollContainer(flow);
832
- if (scroller === null) return null;
833
- const dist = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
834
- return dist <= STICK_BOTTOM_THRESHOLD_PX ? { el: scroller } : null;
835
- }
836
- /** 几何写入后把贴底视口钉回底部:折叠让 scrollHeight 缩小时若不在此帧
837
- * 补写 scrollTop,宿主 ChatView 要到下一帧 ResizeObserver 才吸底回写,
838
- * 中间的空档让触控板惯性滚动乘虚而入,反复折叠时表现为上下抽搐(用户
839
- * 实测:长会话滚到底部附近无法稳定定位)。同一帧内钉回后,宿主的吸底
840
- * 回写成为幂等 no-op,不再是第二个 scrollTop 写入方。 */
841
- stabilizeScrollAfterFold(anchor) {
842
- if (anchor === null) return;
843
- const el = anchor.el;
844
- if (!el.isConnected) return;
845
- const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
846
- if (dist > STICK_BOTTOM_THRESHOLD_PX) el.scrollTop = el.scrollHeight - el.clientHeight;
847
- }
848
- /** flow 元素变化即视为会话切换:完整恢复旧 flow,再从新 DOM 重建。 */
849
- switchFlow(next) {
850
- if (next === this.flow) return;
851
- for (const [el, record] of this.pendingAnims) {
852
- record.anim.cancel();
853
- if (record.kind === "height") this.clearCollapseLock(el);
854
- }
855
- this.pendingAnims.clear();
856
- this.animatableKeys.clear();
857
- this.animatableSegmentBlocks.clear();
858
- for (const record of this.chips.values()) record.chip.remove();
859
- this.chips.clear();
860
- for (const host of [...this.mergedThinks.keys()]) this.removeMergedThink(host);
861
- for (const state of this.segmentStates.values()) state.row?.remove();
862
- this.segmentStates.clear();
863
- this.currentBlocks.clear();
864
- this.blockExpanded.clear();
865
- this.runningSince.clear();
866
- this.completedOnce.clear();
867
- this.bodyTextCache = /* @__PURE__ */ new WeakMap();
868
- this.dirtyMessages.clear();
869
- this.auditDisplays.clear();
870
- this.nativeTurns.clear();
871
- this.emptiedWrappers.clear();
872
- this.structureDirty = true;
873
- this.restoreAllDisplays();
874
- restoreTurnStatus(this.turnStatusTexts);
875
- this.flow = next;
876
- }
877
- createProcessedRow(state) {
878
- const row = createProcessedRowElement(state.duration);
879
- row.addEventListener("click", () => {
880
- state.expanded = !state.expanded;
881
- this.animatableKeys.add(state.key);
882
- this.animatableSegmentBlocks.set(state.key, new Set(state.snapshot.blocks.map((block) => block.key)));
883
- if (state.expanded) {
884
- for (const block of state.snapshot.blocks) {
885
- this.blockExpanded.set(block.key, false);
886
- this.removeMergedThink(block.host);
887
- }
888
- }
889
- this.syncProcessedRow(state);
890
- this.schedule();
891
- });
892
- return row;
893
- }
894
- syncProcessedRow(state) {
895
- const row = state.row;
896
- if (row === null) return;
897
- const text = row.firstElementChild;
898
- const label = state.duration === void 0 ? "\u5DF2\u5904\u7406" : `\u5DF2\u5904\u7406 ${formatDuration(state.duration)}`;
899
- if (text !== null && text.textContent !== label) text.textContent = label;
900
- const expanded = String(state.expanded);
901
- if (row.getAttribute("aria-expanded") !== expanded) row.setAttribute("aria-expanded", expanded);
902
- row.title = state.expanded ? "\u6536\u8D77\u5DE5\u4F5C\u8FC7\u7A0B" : "\u5C55\u5F00\u5DE5\u4F5C\u8FC7\u7A0B";
903
- }
904
- placeProcessedRow(flow, state) {
905
- const row = state.row;
906
- if (row === null) return;
907
- if (!state.snapshot.hasWork || !hasVisibleSegmentWork(state.snapshot)) {
908
- row.remove();
909
- state.row = null;
910
- return;
911
- }
912
- let target = state.snapshot.firstWork ?? state.snapshot.finalStep ?? state.snapshot.boundary;
913
- if (target === null || target.parentElement !== flow) {
914
- row.remove();
915
- state.row = null;
916
- return;
917
- }
918
- while (target.previousElementSibling?.classList.contains("dshcf-flow-chip") === true) {
919
- target = target.previousElementSibling;
920
- }
921
- if (row.parentElement !== flow || row.nextElementSibling !== target) target.before(row);
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
- }
941
- reconcileBlock(block, segment, desiredHidden) {
942
- if (blockNativelyCollapsed(block, this.nativeTurns)) {
943
- this.hideOverlayForNativeCollapse(block);
944
- return;
945
- }
946
- const state = segment === null ? void 0 : this.segmentStates.get(segment.key);
947
- const segmentAnimatableBlocks = segment === null ? void 0 : this.animatableSegmentBlocks.get(segment.key);
948
- const animate = this.animatableKeys.has(block.key) || segment !== null && this.animatableKeys.has(segment.key) && (segmentAnimatableBlocks === void 0 || segmentAnimatableBlocks.has(block.key));
949
- const levelCollapsed = state !== void 0 && !state.expanded;
950
- if (levelCollapsed) {
951
- const keepHost = segment?.finalStep === block.host && this.hasBodyCached(block.host);
952
- let hostFade = false;
953
- if (keepHost) this.restoreElement(block.host);
954
- else hostFade = this.hideElement(block.host, desiredHidden, animate);
955
- for (const container of block.containers) this.hideElement(container, desiredHidden, animate);
956
- for (const row of block.rows) this.hideElement(row, desiredHidden, animate);
957
- const existing = this.chips.get(block.key)?.chip;
958
- if (existing !== void 0 && existing.style.display !== "none") {
959
- existing.style.marginBottom = "";
960
- if (block.mount === "before" || keepHost) {
961
- if (animate && this.canAnimate(existing)) this.startFadeCollapse(existing);
962
- else existing.style.display = "none";
963
- } else if (!hostFade) {
964
- existing.style.display = "none";
965
- }
966
- }
967
- this.syncEmptiedWrappers(block, desiredHidden);
968
- return;
969
- }
970
- let expanded = this.blockExpanded.get(block.key) ?? false;
971
- if (!expanded && block.rows.some((row) => row.hasAttribute("data-selected"))) {
972
- expanded = true;
973
- this.blockExpanded.set(block.key, true);
974
- }
975
- const chip = this.ensureChip(block);
976
- const hostIsCollapsedRow = !expanded && block.rows.includes(block.host);
977
- const hostWasHidden = block.host.style.display === "none";
978
- const hostAnimate = !hostIsCollapsedRow && hostWasHidden && animate;
979
- if (!hostIsCollapsedRow) this.restoreElement(block.host, hostAnimate);
980
- const pendingChip = this.pendingAnims.get(chip);
981
- if (pendingChip?.target === "hidden") this.cancelPendingSync(chip);
982
- const chipWasHidden = chip.style.display === "none";
983
- if (chip.style.display !== "") chip.style.display = "";
984
- if (chipWasHidden && animate && !(hostAnimate && block.mount === "inside")) this.revealVisual(chip);
985
- if (expanded || !this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
986
- const chipSettle = () => {
987
- this.settleFired = true;
988
- if (!this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
989
- };
990
- for (const container of block.containers) {
991
- if (expanded) this.restoreElement(container, animate);
992
- else {
993
- const started = this.hideElement(container, desiredHidden, animate, chipSettle);
994
- if (started) this.pinChipMargin(chip);
995
- }
996
- }
997
- for (const row of block.rows) {
998
- if (expanded) this.restoreElement(row, animate);
999
- else {
1000
- const started = this.hideElement(row, desiredHidden, animate, chipSettle);
1001
- if (started) this.pinChipMargin(chip);
1002
- }
1003
- }
1004
- if (expanded && block.rows.length > 1 && block.rows.every((row) => isThinkRow(row))) {
1005
- this.syncMergedThink(block.host, block.rows, desiredHidden, animate);
1006
- } else {
1007
- if (this.releaseMergedThink(block.host, animate, chipSettle)) this.pinChipMargin(chip);
1008
- }
1009
- this.syncEmptiedWrappers(block, desiredHidden);
1010
- chip.classList.toggle("dshcf-has-body", block.mount === "inside" && this.hasBodyCached(block.host));
1011
- updateChip(chip, block.rows, expanded);
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
- }
1126
- ensureChip(block) {
1127
- let record = this.chips.get(block.key);
1128
- const validParent = record !== void 0 && (block.mount === "inside" ? record.chip.parentElement === block.host : record.chip.parentElement === block.host.parentElement);
1129
- if (record === void 0 || record.host !== block.host || !record.chip.isConnected || !validParent) {
1130
- if (record !== void 0) {
1131
- record.chip.remove();
1132
- this.removeMergedThink(record.host);
1133
- }
1134
- const chip2 = document.createElement("button");
1135
- chip2.type = "button";
1136
- chip2.className = block.mount === "before" ? "dshcf-chip dshcf-flow-chip" : "dshcf-chip";
1137
- chip2.setAttribute("aria-expanded", "false");
1138
- chip2.setAttribute("data-dshcf-block-key", block.key);
1139
- const leading = document.createElement("span");
1140
- leading.className = "dshcf-leading";
1141
- leading.appendChild(createCommandIcon());
1142
- chip2.appendChild(leading);
1143
- chip2.appendChild(createSpan("dshcf-chip-title"));
1144
- chip2.appendChild(createSpan("dshcf-chip-sep"));
1145
- chip2.appendChild(createSpan("dshcf-chip-summary"));
1146
- chip2.appendChild(createChevronIcon("dshcf-chevron"));
1147
- chip2.style.display = "none";
1148
- chip2.addEventListener("click", () => {
1149
- this.blockExpanded.set(block.key, !(this.blockExpanded.get(block.key) ?? false));
1150
- this.animatableKeys.add(block.key);
1151
- this.schedule();
1152
- });
1153
- record = { host: block.host, chip: chip2 };
1154
- this.chips.set(block.key, record);
1155
- }
1156
- const chip = record.chip;
1157
- if (block.mount === "inside") {
1158
- if (chip.parentElement !== block.host || block.host.firstElementChild !== chip) block.host.prepend(chip);
1159
- chip.classList.remove("dshcf-flow-chip");
1160
- } else {
1161
- if (chip.parentElement !== block.host.parentElement || chip.nextElementSibling !== block.host) block.host.before(chip);
1162
- chip.classList.add("dshcf-flow-chip");
1163
- }
1164
- return chip;
1165
- }
1166
- suppressBlock(block, desiredHidden) {
1167
- const existing = this.chips.get(block.key)?.chip;
1168
- if (existing !== void 0 && existing.style.display !== "none") {
1169
- existing.style.marginBottom = "";
1170
- existing.style.display = "none";
1171
- }
1172
- this.removeMergedThink(block.host);
1173
- this.retainDisplayControl(block.host, desiredHidden);
1174
- for (const row of block.rows) this.retainDisplayControl(row, desiredHidden);
1175
- for (const container of block.containers) this.retainDisplayControl(container, desiredHidden);
1176
- }
1177
- retainDisplayControl(el, desiredHidden) {
1178
- if (this.controlledDisplay.has(el)) desiredHidden.add(el);
1179
- }
1180
- cleanupStaleChips(seen) {
1181
- for (const [key, record] of [...this.chips]) {
1182
- if (seen.has(key)) continue;
1183
- record.chip.remove();
1184
- this.removeMergedThink(record.host);
1185
- this.chips.delete(key);
1186
- this.blockExpanded.delete(key);
1187
- }
1188
- }
1189
- /** 连续思考合并行:插在第一个思考行前,标题用第一行思考内容;
1190
- * 点击切换显示/隐藏全部原始思考行。 */
1191
- syncMergedThink(host, rows, desiredHidden, animate = false) {
1192
- let row = this.mergedThinks.get(host);
1193
- if (row === void 0 || !row.isConnected) {
1194
- row = document.createElement("button");
1195
- row.type = "button";
1196
- row.className = "dshcf-merged-think";
1197
- row.setAttribute("aria-expanded", "false");
1198
- const leading = document.createElement("span");
1199
- leading.className = "dshcf-leading";
1200
- leading.appendChild(createThinkIcon());
1201
- const title = document.createElement("span");
1202
- title.className = "dshcf-merged-title";
1203
- const chevron = createChevronIcon("dshcf-chevron");
1204
- row.append(leading, title, chevron);
1205
- row.style.display = "none";
1206
- const btn = row;
1207
- btn.addEventListener("click", () => {
1208
- if (this.mergedThinks.get(host) !== btn) return;
1209
- const next = !this.mergedExpanded.has(host);
1210
- if (next) {
1211
- if (this.expandMergedBody(host, btn)) {
1212
- this.mergedExpanded.add(host);
1213
- btn.setAttribute("aria-expanded", "true");
1214
- }
1215
- } else {
1216
- this.mergedExpanded.delete(host);
1217
- btn.setAttribute("aria-expanded", "false");
1218
- this.collapseMergedBody(host);
1219
- }
1220
- });
1221
- rows[0].before(row);
1222
- this.mergedThinks.set(host, row);
1223
- row = btn;
1224
- }
1225
- const titleEl = row.querySelector(".dshcf-merged-title");
1226
- if (titleEl !== null) {
1227
- let title = this.mergedTitles.get(host);
1228
- if (title === void 0) {
1229
- const first = truncateSummary(stripMarkdown(thinkSummary(rows[0])), 36);
1230
- if (first !== "" && first !== "\u601D\u8003") {
1231
- title = `Think \xB7 ${first}`;
1232
- this.mergedTitles.set(host, title);
1233
- } else {
1234
- title = "\u601D\u8003";
1235
- }
1236
- }
1237
- if (titleEl.textContent !== title) titleEl.textContent = title;
1238
- }
1239
- const expanded = this.mergedExpanded.has(host);
1240
- if (row.getAttribute("aria-expanded") !== String(expanded)) row.setAttribute("aria-expanded", String(expanded));
1241
- const rowWasHidden = row.style.display === "none";
1242
- if (row.style.display !== "") row.style.display = "";
1243
- if (rowWasHidden && animate) this.revealVisual(row);
1244
- for (const r of rows) this.hideElement(r, desiredHidden);
1245
- if (expanded) {
1246
- const result = this.ensureMergedBody(host, row, false);
1247
- if (result !== null && result.created && animate) this.revealMergedBody(result.body);
1248
- }
1249
- }
1250
- /** 展开合并行:直接读各思考行文本合成内容块(不依赖原生行展开:
1251
- * 程序化 click 不触发 React 展开,且后台 tab 的 rAF 不执行)。
1252
- * 返回是否成功——思考行已不可读(parts 为空)时返回 false,调用方
1253
- * 据此保持收起态,避免展开状态与内容块脱节。 */
1254
- expandMergedBody(host, btn) {
1255
- const cached = this.mergedBodyTexts.get(host);
1256
- if (cached === void 0) {
1257
- const parts = this.currentThinkRows(host).map((r) => r.textContent.replace(/^Think\s*/, "").trim()).filter(Boolean);
1258
- if (parts.length === 0) return false;
1259
- this.mergedBodyTexts.set(host, parts.join("\n\n"));
1260
- }
1261
- const result = this.ensureMergedBody(host, btn, true);
1262
- if (result === null) return false;
1263
- if (result.created) {
1264
- this.revealMergedBody(result.body);
1265
- } else {
1266
- this.cancelPendingSync(result.body);
1267
- }
1268
- return true;
1269
- }
1270
- /** 创建/更新合并内容块(缓存优先,不重新展开原生行)。
1271
- * 返回内容块与其是否为本次新建(新建才走展开动画)。 */
1272
- ensureMergedBody(host, btn, force) {
1273
- const cached = this.mergedBodyTexts.get(host);
1274
- if (cached === void 0) return null;
1275
- let body = btn.nextElementSibling;
1276
- let created = false;
1277
- if (body === null || !body.classList.contains("dshcf-merged-body")) {
1278
- const next = document.createElement("div");
1279
- next.className = "dshcf-merged-body";
1280
- btn.after(next);
1281
- body = next;
1282
- created = true;
1283
- }
1284
- if (force || body.textContent !== cached) body.textContent = cached;
1285
- return { body, created };
1286
- }
1287
- /** 清理合并 think 行(v12):状态 map 立即清除;DOM 在手势动画路径下
1288
- * 渐隐后移除(settle 回调),其余路径瞬删。渐隐中途被反向取消时元素
1289
- * 保留,由后续 pass 的 syncMergedThink 重建/复用。
1290
- * settle 透传给每个渐隐目标的移除回调之后(chip 间距钉住的结算探测点,
1291
- * AI 评审 P0:merged 行渐隐不走 block.rows,必须纳入同一钉住体系)。 */
1292
- releaseMergedThink(host, animate = false, settle) {
1293
- const row = this.mergedThinks.get(host);
1294
- this.mergedExpanded.delete(host);
1295
- this.mergedBodyTexts.delete(host);
1296
- if (row === void 0) return false;
1297
- this.mergedThinks.delete(host);
1298
- const body = row.nextElementSibling;
1299
- const targets = body !== null && body.classList.contains("dshcf-merged-body") ? [row, body] : [row];
1300
- if (animate && this.canAnimate(row)) {
1301
- for (const t of targets) this.startFadeCollapse(t, () => {
1302
- t.remove();
1303
- settle?.();
1304
- });
1305
- return true;
1306
- } else {
1307
- for (const t of targets) t.remove();
1308
- return false;
1309
- }
1310
- }
1311
- /** merged-body 展开高度动画(机制样板:插件全资 DOM)。
1312
- * 关键帧含 marginBottom 0→16px——其 CSS 有常量 margin-bottom:16px,
1313
- * 高度从 0 起步时这 16px 会先占位产生小跳变。fill:'forwards' 托住终态,
1314
- * onfinish 清内联后 cancel 释放,无闪烁窗口。收起由 collapseMergedBody
1315
- * 做镜像高度卷下(同款账本与身份守卫),开合对称。 */
1316
- revealMergedBody(body) {
1317
- if (!this.canAnimate(body)) return;
1318
- this.cancelPendingSync(body);
1319
- const targetHeight = body.getBoundingClientRect().height;
1320
- if (!(targetHeight > 0)) return;
1321
- body.style.height = "0px";
1322
- body.style.overflow = "hidden";
1323
- body.style.marginBottom = "0px";
1324
- const anim = body.animate(
1325
- [
1326
- { height: "0px", marginBottom: "0px" },
1327
- { height: `${targetHeight}px`, marginBottom: "16px" }
1328
- ],
1329
- { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1330
- );
1331
- const record = { anim, target: "visible", kind: "height" };
1332
- this.pendingAnims.set(body, record);
1333
- anim.onfinish = () => {
1334
- if (this.pendingAnims.get(body) !== record) return;
1335
- this.pendingAnims.delete(body);
1336
- body.style.height = "";
1337
- body.style.overflow = "";
1338
- body.style.marginBottom = "";
1339
- anim.cancel();
1340
- this.schedule();
1341
- };
1342
- anim.oncancel = () => {
1343
- if (this.pendingAnims.get(body) !== record) return;
1344
- this.pendingAnims.delete(body);
1345
- };
1346
- }
1347
- /** 收起合并行:内容块高度卷下后移除——镜像 revealMergedBody 的唯一几何动画,
1348
- * 开合对称。插件全资静态文本 DOM、无 React 协调竞争,可安全做几何收起
1349
- * (与 seat 级拒绝盲卷的场景不同:那里是 React 混杂多卡片)。
1350
- * reduced-motion / 无 WAAPI / 零高度降级为同步 remove()。 */
1351
- collapseMergedBody(host) {
1352
- const btn = this.mergedThinks.get(host);
1353
- if (btn === void 0) return;
1354
- const body = btn.nextElementSibling;
1355
- if (body === null || !body.classList.contains("dshcf-merged-body")) return;
1356
- const el = body;
1357
- if (!this.canAnimate(el)) {
1358
- el.remove();
1359
- return;
1360
- }
1361
- this.cancelPendingSync(el);
1362
- const current = el.getBoundingClientRect().height;
1363
- if (!(current > 0)) {
1364
- el.remove();
1365
- return;
1366
- }
1367
- el.style.height = `${current}px`;
1368
- el.style.overflow = "hidden";
1369
- const anim = el.animate(
1370
- [
1371
- { height: `${current}px`, marginBottom: "16px" },
1372
- { height: "0px", marginBottom: "0px" }
1373
- ],
1374
- { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1375
- );
1376
- const record = { anim, target: "hidden", kind: "height" };
1377
- this.pendingAnims.set(el, record);
1378
- anim.onfinish = () => {
1379
- if (this.pendingAnims.get(el) !== record) return;
1380
- this.pendingAnims.delete(el);
1381
- el.remove();
1382
- anim.cancel();
1383
- this.schedule();
1384
- };
1385
- anim.oncancel = () => {
1386
- if (this.pendingAnims.get(el) !== record) return;
1387
- this.pendingAnims.delete(el);
1388
- this.clearCollapseLock(el);
1389
- };
1390
- }
1391
- /** 当前宿主内的思考行(现取,React 重渲染后引用仍然有效)。 */
1392
- currentThinkRows(host) {
1393
- return [...host.querySelectorAll('[data-variant="think"]:not([data-tool])')].filter(
1394
- (r) => r.closest("[data-chat-call-id]") === null && r.closest("[data-subcalls]") === null
1395
- );
1396
- }
1397
- /** 移除合并思考行(二级收起 / 一级收起时),恢复行由 applyRows 控制。
1398
- * 合并内容块(btn 的兄弟节点)一并移除,避免宿主展开后残留文本。 */
1399
- removeMergedThink(host) {
1400
- const row = this.mergedThinks.get(host);
1401
- if (row !== void 0) {
1402
- const body = row.nextElementSibling;
1403
- if (body !== null && body.classList.contains("dshcf-merged-body")) body.remove();
1404
- row.remove();
1405
- this.mergedThinks.delete(host);
1406
- }
1407
- this.mergedExpanded.delete(host);
1408
- this.mergedBodyTexts.delete(host);
1409
- }
1410
- /** 正文判定(带缓存):同一消息子树未变时直接复用上次结果。失效由
1411
- * markDirty(mutation 定向)与 switchFlow(整体重置)驱动;缓存的是
1412
- * 纯文本/媒体存在性判定,与 display 状态无关,插件自身的显隐切换
1413
- * 不会产生脏数据。 */
1414
- hasBodyCached(el) {
1415
- const cached = this.bodyTextCache.get(el);
1416
- if (cached !== void 0) return cached;
1417
- const value = hasBodyContent(el);
1418
- this.bodyTextCache.set(el, value);
1419
- return value;
1420
- }
1421
- /** 本块是否有在途收起渐隐(rows/containers/merged 行/body 任一)。
1422
- * 基于 pendingAnims 账本无状态判定:onfinish/oncancel 都会即时清账,
1423
- * 取消路径天然解锁(计数器/最后注册者会卡死)。merged 行渐隐时已被
1424
- * releaseMergedThink 摘出 mergedThinks,按 DOM 类名现查。 */
1425
- hasPendingCollapse(block) {
1426
- const check = (el) => el !== null && el !== void 0 && this.pendingAnims.get(el)?.target === "hidden";
1427
- if (block.containers.some(check)) return true;
1428
- if (block.rows.some(check)) return true;
1429
- const mergedRow = block.host.querySelector(".dshcf-merged-think");
1430
- if (check(mergedRow)) return true;
1431
- const mergedBody = mergedRow?.nextElementSibling;
1432
- if (mergedBody instanceof HTMLElement && check(mergedBody)) return true;
1433
- return false;
1434
- }
1435
- /** 钉住 chip 与首行的 16px 间距(收起 fade 期间;内联优先于 aria=false 的 0)。
1436
- * flow-chip(context 等 before-mounted)豁免:其间距由宿主 row-gap 16px
1437
- * 提供、自身 CSS 恒 0,钉住 16px 会叠加成 32px(真机实测:收起上下文
1438
- * 注入时二级与三级间距瞬间扩大)。
1439
- */
1440
- pinChipMargin(chip) {
1441
- if (chip.classList.contains("dshcf-flow-chip")) return;
1442
- if (chip.style.marginBottom !== "16px") chip.style.marginBottom = "16px";
1443
- }
1444
- /** 解除钉住(aria=true 的 16px 或 aria=false 的 0 由 CSS 接管)。 */
1445
- unpinChipMargin(chip) {
1446
- if (chip.style.marginBottom !== "") chip.style.marginBottom = "";
1447
- }
1448
- /** 外部介入检测(issue #11 Bug A):当前内联值 ≠ 插件最后确保值,或所有权
1449
- * 哨兵被 style 整体改写抹除。返回 true 时调用方放弃本次写回并交还账本——
1450
- * 属性级改写由值比对捕获,整体改写(cssText / setAttribute('style'))由
1451
- * 哨兵缺失捕获,两层合起来覆盖外部介入的两种形态。 */
1452
- displayForeign(el) {
1453
- const written = this.writtenDisplay.get(el);
1454
- if (written === void 0) return false;
1455
- return el.style.getPropertyValue(DISPLAY_OWNED_PROP) === "" || el.style.display !== written;
1456
- }
1457
- /** 清空单个元素的显示账本(三账本 + 所有权哨兵)。 */
1458
- releaseDisplayLedger(el) {
1459
- this.originalDisplay.delete(el);
1460
- this.writtenDisplay.delete(el);
1461
- this.controlledDisplay.delete(el);
1462
- el.style.removeProperty(DISPLAY_OWNED_PROP);
1463
- }
1464
- /** 返回 true 表示启动了渐隐动画(调用方可据此决定内部元素的处置)。
1465
- * settle 在渐隐自然结束时调用(onfinish 链;反向取消不触发)。 */
1466
- hideElement(el, desired, animate = false, settle) {
1467
- desired.add(el);
1468
- const pending = this.pendingAnims.get(el);
1469
- if (pending !== void 0) {
1470
- if (pending.target === "hidden") return false;
1471
- this.cancelPendingSync(el);
1472
- }
1473
- if (!this.originalDisplay.has(el) && !isDisplayed(el)) return false;
1474
- if (this.hasAnimatingAncestor(el)) return false;
1475
- if (!this.originalDisplay.has(el) || this.displayForeign(el)) {
1476
- this.originalDisplay.set(el, el.style.display);
1477
- this.writtenDisplay.set(el, el.style.display);
1478
- el.style.setProperty(DISPLAY_OWNED_PROP, "1");
1479
- }
1480
- this.controlledDisplay.add(el);
1481
- if (el.style.display === "none") return false;
1482
- if (animate && this.canAnimate(el)) {
1483
- this.startFadeCollapse(el, settle);
1484
- return true;
1485
- }
1486
- el.style.display = "none";
1487
- this.writtenDisplay.set(el, "none");
1488
- this.displayTouched = true;
1489
- return false;
1490
- }
1491
- restoreElement(el, animate = false) {
1492
- const pending = this.pendingAnims.get(el);
1493
- if (pending !== void 0) {
1494
- if (pending.target === "visible") return;
1495
- this.cancelPendingSync(el);
1496
- }
1497
- if (!this.originalDisplay.has(el)) return;
1498
- if (this.displayForeign(el)) {
1499
- this.releaseDisplayLedger(el);
1500
- return;
1501
- }
1502
- const original = this.originalDisplay.get(el);
1503
- if (!animate || !this.canAnimate(el) || this.hasAnimatingAncestor(el)) {
1504
- if (el.style.display !== original) {
1505
- el.style.display = original;
1506
- this.displayTouched = true;
1507
- }
1508
- this.releaseDisplayLedger(el);
1509
- return;
1510
- }
1511
- if (el.style.display !== original) {
1512
- el.style.display = original;
1513
- this.displayTouched = true;
1514
- }
1515
- this.writtenDisplay.set(el, original);
1516
- this.startReveal(el);
1517
- }
1518
- /** 是否可动画:WAAPI 特性检测 + reduced-motion 门控(均做 typeof 防桩缺失)。 */
1519
- canAnimate(el) {
1520
- if (typeof el.animate !== "function") return false;
1521
- if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
1522
- return true;
1523
- }
1524
- /** 展开方向淡入(opacity + 4px 微位移):无高度分量、零布局读取。
1525
- * onfinish 按终态可见对齐账本(双删除)并 schedule() 幂等重同步;
1526
- * oncancel 只做身份守卫删除——取消方的终态写入自己负责。 */
1527
- startReveal(el) {
1528
- const anim = el.animate(
1529
- [
1530
- { opacity: "0", transform: "translateY(4px)" },
1531
- { opacity: "1", transform: "translateY(0)" }
1532
- ],
1533
- { duration: ANIM_DURATION_MS, easing: ANIM_EASING }
1534
- );
1535
- const record = { anim, target: "visible", kind: "fade" };
1536
- this.pendingAnims.set(el, record);
1537
- anim.onfinish = () => {
1538
- if (this.pendingAnims.get(el) !== record) return;
1539
- this.pendingAnims.delete(el);
1540
- this.releaseDisplayLedger(el);
1541
- this.schedule();
1542
- };
1543
- anim.oncancel = () => {
1544
- if (this.pendingAnims.get(el) !== record) return;
1545
- this.pendingAnims.delete(el);
1546
- };
1547
- }
1548
- /** 同步取消在途动画并清账:收起动画需同时清锁高内联(height/overflow/
1549
- * marginBottom),否则取消方写完终态后元素仍被锁高裁剪一帧以上。 */
1550
- cancelPendingSync(el) {
1551
- const pending = this.pendingAnims.get(el);
1552
- if (pending === void 0) return;
1553
- pending.anim.cancel();
1554
- this.pendingAnims.delete(el);
1555
- if (pending.kind === "height") this.clearCollapseLock(el);
36
+ // src/host-contract.ts
37
+ var FLOW = "[data-chat-flow]";
38
+ var SEAT = "[data-chat-flow-kind]";
39
+ var OWNED = "[data-dshcf-group]";
40
+ var FOLDED = "data-dshcf-folded";
41
+ var NATIVE_HIDDEN = "[data-turn-process-hidden], [data-turn-process-inline]";
42
+ function elementOf(node) {
43
+ return node.nodeType === 1 ? node : node.parentElement;
44
+ }
45
+ function pluginOwned(node) {
46
+ return elementOf(node)?.closest(OWNED) != null;
47
+ }
48
+ function seatOf(node, flow) {
49
+ let element = elementOf(node);
50
+ while (element !== null && element.parentElement !== flow) element = element.parentElement;
51
+ return element instanceof HTMLElement && element.matches(SEAT) ? element : null;
52
+ }
53
+ function seatKey(seat) {
54
+ return seat.getAttribute("data-chat-flow-key") ?? seat.getAttribute("data-chat-anchor-key");
55
+ }
56
+ function nativeHidden(row, flow) {
57
+ const owner = row.closest(NATIVE_HIDDEN);
58
+ return owner !== null && flow.contains(owner);
59
+ }
60
+ function nativeOwnsHidden(element) {
61
+ return element.matches(NATIVE_HIDDEN);
62
+ }
63
+ function findFlow() {
64
+ for (const flow of document.querySelectorAll(FLOW)) {
65
+ if (flow.getClientRects().length > 0 && getComputedStyle(flow).visibility !== "hidden") return flow;
1556
66
  }
1557
- clearCollapseLock(el) {
1558
- el.style.height = "";
1559
- el.style.overflow = "";
1560
- el.style.marginBottom = "";
1561
- el.style.boxSizing = "";
67
+ return null;
68
+ }
69
+ function nativeControls(flow) {
70
+ const result = /* @__PURE__ */ new Map();
71
+ for (const button of flow.querySelectorAll("button[data-turn-process]")) {
72
+ const turn = button.getAttribute("data-turn-process");
73
+ if (turn !== null && button.closest('[data-chat-flow-kind="turn-process"]') !== null) result.set(turn, button);
1562
74
  }
1563
- /** 祖先 seat 在途动画检测:沿 parentNode 走到 flow,任一祖先在 pendingAnims
1564
- * 即视为在途。分层规则——同一视觉变化只动画一层。 */
1565
- hasAnimatingAncestor(el) {
1566
- const flow = this.flow;
1567
- if (flow === null) return false;
1568
- let node = el.parentElement;
1569
- while (node !== null && node !== flow) {
1570
- if (this.pendingAnims.has(node)) return true;
1571
- node = node.parentElement;
75
+ return result;
76
+ }
77
+ function nativeCollapsed(button) {
78
+ if (button === void 0 || !button.isConnected) return false;
79
+ const expanded = button.getAttribute("aria-expanded");
80
+ return expanded === "false" || expanded === null && !button.hasAttribute("data-open");
81
+ }
82
+ function openNative(button) {
83
+ if (button !== void 0 && nativeCollapsed(button)) {
84
+ const focused = button.ownerDocument.activeElement;
85
+ button.click();
86
+ if (focused instanceof HTMLElement && focused.isConnected && focused !== button) {
87
+ focused.focus({ preventScroll: true });
88
+ if (focused === button.ownerDocument.body && button.ownerDocument.activeElement === button) button.blur();
1572
89
  }
1573
- return false;
1574
90
  }
1575
- /** 收起方向渐隐动画(v11 定稿):镜像 reveal 的 opacity + 4px 微位移,
1576
- * 淡完 onfinish 写 display:none 并保持双条目(镜像 hideElement 终态契约)。
1577
- * fill:'forwards' 占位到终态写入后释放;无几何锁、无 gap 补偿。 */
1578
- startFadeCollapse(el, settle) {
1579
- const anim = el.animate(
1580
- [
1581
- { opacity: "1", transform: "translateY(0)" },
1582
- { opacity: "0", transform: "translateY(4px)" }
1583
- ],
1584
- { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1585
- );
1586
- const record = { anim, target: "hidden", kind: "fade", settle };
1587
- this.pendingAnims.set(el, record);
1588
- anim.onfinish = () => this.finishFadeCollapse(el, record);
1589
- anim.oncancel = () => {
1590
- if (this.pendingAnims.get(el) !== record) return;
1591
- this.pendingAnims.delete(el);
91
+ }
92
+ var OBSERVED_ATTRIBUTES = [
93
+ "data-chat-flow-key",
94
+ "data-chat-anchor-key",
95
+ "data-chat-flow-kind",
96
+ "data-chat-turn",
97
+ "data-variant",
98
+ "data-tool",
99
+ "data-state",
100
+ "data-selected",
101
+ "data-open",
102
+ "data-expanded",
103
+ "aria-expanded",
104
+ "data-turn-process-member",
105
+ "data-turn-process-hidden",
106
+ "data-turn-process-inline",
107
+ "hidden",
108
+ "style",
109
+ "class"
110
+ ];
111
+
112
+ // src/work-model.ts
113
+ var ASSISTANT_KINDS = /* @__PURE__ */ new Set(["assistant-step", "assistant"]);
114
+ var CONTEXT_KINDS = /* @__PURE__ */ new Set(["context", "system-prompt"]);
115
+ var COMMAND_KINDS = /* @__PURE__ */ new Set(["command", "manual-compaction"]);
116
+ function readSeat(seat, fallback) {
117
+ const kind = seat.getAttribute("data-chat-flow-kind");
118
+ const key = seatKey(seat);
119
+ const turn = seat.getAttribute("data-chat-turn") || null;
120
+ const result = { tokens: [], items: [] };
121
+ if (kind === "turn-process") return result;
122
+ if (key === null || kind === null) {
123
+ result.tokens.push({ type: "boundary", id: `unknown:${key ?? fallback}`, turn: null, hard: true });
124
+ return result;
125
+ }
126
+ const boundary = (id, hard) => ({ type: "boundary", id: `${key}:${id}`, turn, hard });
127
+ const add = (row, workKind, suffix) => {
128
+ const item = {
129
+ id: JSON.stringify([key, workKind, suffix]),
130
+ turn,
131
+ kind: workKind,
132
+ seat,
133
+ row,
134
+ cover: row,
135
+ followNativeTurn: kind === "system-prompt" && turn !== null
1592
136
  };
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
- }
1628
- /** 轻量视觉 reveal(opacity + 4px 微位移):用于插件全资元素的即时显示
1629
- * 路径——chip(一级展开时出现)与 merged-think 行(二级展开时出现)。
1630
- * 这些元素的 display 完全由插件直写、无 React 协调竞争,因此不入
1631
- * pendingAnims 账本、无仲裁;收起同为直写 display:none,无 fill 的在途
1632
- * 动画残留在隐藏元素上自然失效。门控沿用 animate 布尔(手势路径才调)。 */
1633
- revealVisual(el) {
1634
- if (!this.canAnimate(el)) return;
1635
- el.animate(
1636
- [
1637
- { opacity: "0", transform: "translateY(4px)" },
1638
- { opacity: "1", transform: "translateY(0)" }
1639
- ],
1640
- { duration: ANIM_DURATION_MS, easing: ANIM_EASING }
1641
- );
1642
- }
1643
- restoreUnusedDisplays(desired) {
1644
- for (const el of [...this.controlledDisplay]) {
1645
- if (desired.has(el)) continue;
1646
- if (!el.isConnected && this.pendingAnims.get(el)?.target === "hidden") {
1647
- this.releaseDisplayLedger(el);
137
+ result.items.push(item);
138
+ return item;
139
+ };
140
+ const token = (item) => ({ type: "work", id: item.id, turn, kind: item.kind });
141
+ if (CONTEXT_KINDS.has(kind) || COMMAND_KINDS.has(kind)) {
142
+ if (seat.childNodes.length === 0) return result;
143
+ const item = add(seat, CONTEXT_KINDS.has(kind) ? "context" : "command", "");
144
+ result.tokens.push(token(item));
145
+ return result;
146
+ }
147
+ if (!ASSISTANT_KINDS.has(kind) && kind !== "tool-call") {
148
+ result.tokens.push(boundary("fence", true));
149
+ return result;
150
+ }
151
+ const roots = /* @__PURE__ */ new Map();
152
+ const paths = /* @__PURE__ */ new Set([seat]);
153
+ const candidates = [...seat.querySelectorAll('[data-chat-call-id], [data-variant="think"]')];
154
+ let thinkIndex = 0;
155
+ for (const row of candidates) {
156
+ if (pluginOwned(row) || row.closest("[data-subcalls]") !== null) continue;
157
+ const outerCall = row.parentElement?.closest("[data-chat-call-id]");
158
+ if (outerCall !== null && outerCall !== void 0 && seat.contains(outerCall)) continue;
159
+ const isTool = row.hasAttribute("data-chat-call-id");
160
+ if (!isTool && row.hasAttribute("data-tool")) continue;
161
+ if (row.querySelector("[data-disclosure-row]") === null) continue;
162
+ const item = add(row, isTool ? "tool" : "think", isTool ? row.getAttribute("data-chat-call-id") : String(thinkIndex++));
163
+ roots.set(row, item);
164
+ for (let parent = row.parentElement; parent !== null && seat.contains(parent); parent = parent.parentElement) paths.add(parent);
165
+ }
166
+ let bodyIndex = 0;
167
+ const visit = (node) => {
168
+ if (pluginOwned(node) || node.nodeType === 8) return [];
169
+ if (node.nodeType === 3) return (node.textContent ?? "").trim() === "" ? [] : [boundary(`body:${bodyIndex++}`, false)];
170
+ if (!(node instanceof Element)) return [];
171
+ if (node.matches("script, style, template")) return [];
172
+ const work = roots.get(node);
173
+ if (work !== void 0) return [token(work)];
174
+ if (!paths.has(node)) {
175
+ return [boundary(`body:${bodyIndex++}`, false)];
176
+ }
177
+ const children = [...node.childNodes].flatMap(visit);
178
+ if (children.length > 0 && children.every((child) => child.type === "work") && node instanceof HTMLElement) {
179
+ const ids = new Set(children.map((child) => child.id));
180
+ for (const item of result.items) if (ids.has(item.id)) item.cover = node;
181
+ }
182
+ return children;
183
+ };
184
+ result.tokens = visit(seat);
185
+ return result;
186
+ }
187
+ var WorkModel = class {
188
+ constructor() {
189
+ this.seats = /* @__PURE__ */ new Map();
190
+ this.identities = /* @__PURE__ */ new WeakMap();
191
+ this.sequence = 0;
192
+ }
193
+ read(flow, dirty, force = false) {
194
+ const live = /* @__PURE__ */ new Set();
195
+ const tokens = [];
196
+ const items = /* @__PURE__ */ new Map();
197
+ for (const child of flow.children) {
198
+ if (!(child instanceof HTMLElement) || pluginOwned(child)) continue;
199
+ if (!child.hasAttribute("data-chat-flow-kind")) {
1648
200
  continue;
1649
201
  }
1650
- this.restoreElement(el);
1651
- }
1652
- }
1653
- /** 收集 audit 需要观察的 display 集合;只读取内联样式,不触发布局计算。 */
1654
- collectAuditDisplays(flow) {
1655
- const nodes = new Set(flowItems(flow));
1656
- for (const el of this.controlledDisplay) {
1657
- if (el.isConnected && nodeWithin(el, flow)) nodes.add(el);
1658
- }
1659
- for (const { chip } of this.chips.values()) {
1660
- if (chip.isConnected && nodeWithin(chip, flow)) nodes.add(chip);
1661
- }
1662
- for (const row of this.mergedThinks.values()) {
1663
- if (row.isConnected && nodeWithin(row, flow)) nodes.add(row);
202
+ live.add(child);
203
+ let snapshot = this.seats.get(child);
204
+ if (force || snapshot === void 0 || dirty.has(child)) {
205
+ let identity = this.identities.get(child);
206
+ if (identity === void 0) {
207
+ identity = String(++this.sequence);
208
+ this.identities.set(child, identity);
209
+ }
210
+ snapshot = readSeat(child, identity);
211
+ this.seats.set(child, snapshot);
212
+ }
213
+ tokens.push(...snapshot.tokens);
214
+ for (const item of snapshot.items) items.set(item.id, item);
1664
215
  }
1665
- const displays = /* @__PURE__ */ new Map();
1666
- for (const el of nodes) displays.set(el, el.style.display);
1667
- return displays;
216
+ for (const seat of this.seats.keys()) if (!live.has(seat)) this.seats.delete(seat);
217
+ return { tokens, items };
1668
218
  }
1669
- /** 在完整 pass 完成后保存 display 基线,供下一次轻量 audit 比对。 */
1670
- captureAuditDisplays(flow) {
1671
- this.auditDisplays = this.collectAuditDisplays(flow);
1672
- }
1673
- restoreAllDisplays() {
1674
- for (const el of [...this.controlledDisplay]) this.restoreElement(el);
1675
- this.controlledDisplay.clear();
1676
- this.originalDisplay = /* @__PURE__ */ new WeakMap();
1677
- this.writtenDisplay = /* @__PURE__ */ new WeakMap();
219
+ clear() {
220
+ this.seats.clear();
1678
221
  }
1679
222
  };
1680
- function createSpan(cls) {
1681
- const span = document.createElement("span");
1682
- span.className = cls;
1683
- return span;
1684
- }
1685
- var COMMAND_ICON_PATHS = [
1686
- {
1687
- transform: "translate(0.6689 1.073)",
1688
- 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"
1689
- },
1690
- {
1691
- transform: "translate(0.6689 1.073)",
1692
- 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"
1693
- },
1694
- {
1695
- transform: "translate(0.6689 1.073)",
1696
- d: "M9.23789 7.35397L9.23789 8.53488L6.96238 8.53488L6.96238 7.35397L9.23789 7.35397Z"
1697
- }
1698
- ];
1699
- 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";
1700
- function createChevronIcon(className) {
1701
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1702
- svg.setAttribute("width", "14");
1703
- svg.setAttribute("height", "14");
1704
- svg.setAttribute("class", className);
1705
- svg.setAttribute("viewBox", "0 0 14 14");
1706
- svg.setAttribute("fill", "none");
1707
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1708
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1709
- path.setAttribute("d", NATIVE_CHEVRON_DOWN_PATH);
1710
- path.setAttribute("fill", "currentColor");
1711
- svg.appendChild(path);
1712
- return svg;
223
+
224
+ // src/work-groups.ts
225
+ function resolveTurns(tokens) {
226
+ const nextTurns = new Array(tokens.length).fill(null);
227
+ let next = null;
228
+ for (let i = tokens.length - 1; i >= 0; i--) {
229
+ const token = tokens[i];
230
+ if (token.type === "boundary" && token.hard) next = null;
231
+ nextTurns[i] = next;
232
+ if (!(token.type === "boundary" && token.hard) && token.turn !== null) next = token.turn;
233
+ }
234
+ let previous = null;
235
+ return tokens.map((token, i) => {
236
+ if (token.type === "boundary" && token.hard) {
237
+ previous = null;
238
+ return token;
239
+ }
240
+ if (token.turn !== null) {
241
+ previous = token.turn;
242
+ return token;
243
+ }
244
+ if (token.type !== "work") return token;
245
+ const following = nextTurns[i];
246
+ const turn = previous === null ? following : following === null || previous === following ? previous : null;
247
+ return { ...token, turn };
248
+ });
1713
249
  }
1714
- var THINK_ICON_PATHS = [
1715
- {
1716
- d: "M7.06431 5.93342C7.68763 5.93342 8.19307 6.43904 8.19322 7.06233C8.19322 7.68573 7.68772 8.19123 7.06431 8.19123C6.44099 8.19113 5.9354 7.68567 5.9354 7.06233C5.93555 6.43911 6.44108 5.93353 7.06431 5.93342Z"
1717
- },
1718
- {
1719
- evenodd: true,
1720
- d: "M8.6815 0.963693C10.1169 0.447019 11.6266 0.374829 12.5633 1.31135C13.5 2.24805 13.4277 3.75776 12.911 5.19319C12.7126 5.74431 12.4386 6.31796 12.0965 6.89729C12.4969 7.54638 12.8141 8.19018 13.036 8.80647C13.5527 10.2419 13.6251 11.7516 12.6883 12.6883C11.7516 13.625 10.242 13.5527 8.8065 13.036C8.19022 12.8141 7.54641 12.4969 6.89732 12.0965C6.31797 12.4386 5.74435 12.7125 5.19322 12.911C3.75777 13.4276 2.2481 13.5 1.31138 12.5633C0.374859 11.6266 0.447049 10.1168 0.963724 8.68147C1.17185 8.10338 1.46321 7.50063 1.82896 6.8924C1.52182 6.35711 1.27235 5.82825 1.08872 5.31819C0.572068 3.88278 0.499714 2.37306 1.43638 1.43635C2.37308 0.499655 3.8828 0.572044 5.31822 1.08869C5.82828 1.27232 6.35715 1.5218 6.89243 1.82893C7.50066 1.46318 8.10341 1.17181 8.6815 0.963693ZM11.3573 8.01154C10.9083 8.62253 10.3901 9.22873 9.80943 9.8094C9.22877 10.3901 8.62255 10.9083 8.01158 11.3572C8.4257 11.5841 8.8287 11.7688 9.21275 11.9071C10.5456 12.3868 11.4246 12.2547 11.8397 11.8397C12.2548 11.4246 12.3869 10.5456 11.9071 9.21272C11.7688 8.82866 11.5841 8.42568 11.3573 8.01154ZM2.56529 8.02912C2.37344 8.39322 2.21495 8.74796 2.09263 9.08772C1.61291 10.4204 1.74512 11.2995 2.16001 11.7147C2.57505 12.1297 3.45415 12.2618 4.78697 11.7821C5.11057 11.6656 5.44786 11.5164 5.7938 11.3367C5.249 10.9223 4.70922 10.4533 4.19029 9.9344C3.57578 9.31987 3.03169 8.67633 2.56529 8.02912ZM6.90708 3.2469C6.24065 3.70479 5.5646 4.26321 4.91392 4.91389C4.26325 5.56456 3.70482 6.24063 3.24693 6.90705C3.72674 7.63325 4.32777 8.37459 5.03892 9.08576C5.64943 9.69627 6.28183 10.2265 6.90806 10.6678C7.59368 10.2025 8.2908 9.63076 8.96079 8.96076C9.6308 8.29075 10.2025 7.59366 10.6678 6.90803C10.2265 6.2818 9.69631 5.6494 9.08579 5.03889C8.37462 4.32773 7.63328 3.72672 6.90708 3.2469ZM11.7147 2.15998C11.2996 1.74509 10.4204 1.61288 9.08775 2.0926C8.74835 2.21479 8.39382 2.37271 8.03013 2.56428C8.67728 3.03065 9.31995 3.5758 9.93443 4.19026C10.4534 4.7092 10.9223 5.24896 11.3368 5.79377C11.5164 5.44785 11.6656 5.11052 11.7821 4.78694C12.2618 3.45416 12.1297 2.57502 11.7147 2.15998ZM4.91197 2.2176C3.57922 1.73788 2.70004 1.86995 2.28501 2.28498C1.87001 2.70003 1.73791 3.5792 2.21763 4.91194C2.31709 5.18822 2.44112 5.47427 2.58677 5.7674C3.01931 5.1887 3.51474 4.6158 4.06529 4.06526C4.61584 3.5147 5.18872 3.01928 5.76743 2.58674C5.47431 2.4411 5.18824 2.31706 4.91197 2.2176Z"
1721
- }
1722
- ];
1723
- function findNativeThinkSvg() {
1724
- for (const drow of document.querySelectorAll('[data-variant="think"] [data-disclosure-row]')) {
1725
- for (const svg of drow.querySelectorAll("svg")) {
1726
- if (svg.querySelectorAll("path").length >= 2) return svg;
250
+ function groupWork(tokens) {
251
+ const groups = [];
252
+ let left = "start";
253
+ let current = null;
254
+ let lastItem = "";
255
+ for (const token of resolveTurns(tokens)) {
256
+ if (token.type === "boundary") {
257
+ left = token.id;
258
+ current = null;
259
+ continue;
1727
260
  }
1728
- }
1729
- return null;
1730
- }
1731
- function createThinkIcon() {
1732
- const native = findNativeThinkSvg();
1733
- if (native !== null) return native.cloneNode(true);
1734
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1735
- svg.setAttribute("viewBox", "0 0 14 14");
1736
- svg.setAttribute("width", "14");
1737
- svg.setAttribute("height", "14");
1738
- svg.setAttribute("fill", "none");
1739
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1740
- for (const p of THINK_ICON_PATHS) {
1741
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1742
- if (p.evenodd === true) {
1743
- path.setAttribute("fill-rule", "evenodd");
1744
- path.setAttribute("clip-rule", "evenodd");
261
+ if (current !== null && current.turn !== token.turn) {
262
+ left = `turn-after:${lastItem}`;
263
+ current = null;
1745
264
  }
1746
- path.setAttribute("d", p.d);
1747
- path.setAttribute("fill", "currentColor");
1748
- svg.appendChild(path);
1749
- }
1750
- return svg;
1751
- }
1752
- function findNativeCommandSvg() {
1753
- const selector = '[data-chat-call-id] [data-disclosure-row], [data-chat-flow-kind="command"] [data-disclosure-row], [data-chat-flow-kind="manual-compaction"] [data-disclosure-row]';
1754
- for (const drow of document.querySelectorAll(selector)) {
1755
- for (const svg of drow.querySelectorAll("svg")) {
1756
- if (svg.querySelectorAll("path").length === 3 && isIcon14(svg)) return svg;
265
+ if (current === null) {
266
+ current = { id: JSON.stringify([left, token.turn]), turn: token.turn, items: [] };
267
+ groups.push(current);
1757
268
  }
269
+ current.items.push(token.id);
270
+ lastItem = token.id;
1758
271
  }
1759
- return null;
1760
- }
1761
- function isIcon14(svg) {
1762
- if (svg.getAttribute("width") === "14" && svg.getAttribute("height") === "14") return true;
1763
- const vb = (svg.getAttribute("viewBox") ?? "").trim().split(/\s+/);
1764
- return vb.length === 4 && Number(vb[2]) === 14 && Number(vb[3]) === 14;
1765
- }
1766
- var cachedNativeCommandSvg = null;
1767
- function createCommandIcon() {
1768
- if (cachedNativeCommandSvg !== null) return cachedNativeCommandSvg.cloneNode(true);
1769
- const native = findNativeCommandSvg();
1770
- if (native !== null) {
1771
- cachedNativeCommandSvg = native;
1772
- return native.cloneNode(true);
1773
- }
1774
- return createCommandIconFallback();
272
+ return groups;
1775
273
  }
1776
- function createCommandIconFallback() {
1777
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1778
- svg.setAttribute("viewBox", "0 0 14 14");
1779
- svg.setAttribute("width", "14");
1780
- svg.setAttribute("height", "14");
1781
- svg.setAttribute("fill", "none");
1782
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1783
- for (const p of COMMAND_ICON_PATHS) {
1784
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1785
- path.setAttribute("transform", p.transform);
1786
- path.setAttribute("d", p.d);
1787
- path.setAttribute("fill", "currentColor");
1788
- svg.appendChild(path);
274
+ var GroupState = class {
275
+ constructor() {
276
+ this.groups = /* @__PURE__ */ new Map();
277
+ this.expanded = /* @__PURE__ */ new Map();
1789
278
  }
1790
- return svg;
1791
- }
1792
- var CONTEXT_ICON_PATHS = [
1793
- {
1794
- d: "M11.2426 4.80473V6.10551H4.75819V4.80473H11.2426Z"
1795
- },
1796
- {
1797
- d: "M9.40858 7.84478V9.14557H4.75819V7.84478H9.40858Z"
1798
- },
1799
- {
1800
- d: "M9.23438 0.546389C10.1941 0.546389 10.9683 0.544914 11.5859 0.611819C12.2161 0.680096 12.7634 0.825745 13.2393 1.17139C13.5172 1.3733 13.7619 1.61812 13.9639 1.896C14.3096 2.37183 14.4551 2.91922 14.5234 3.54932C14.5903 4.16686 14.5889 4.94133 14.5889 5.90088V10.0981C14.5889 11.0576 14.5903 11.8321 14.5234 12.4497C14.4552 13.0798 14.3094 13.6272 13.9639 14.103C13.7619 14.381 13.5172 14.6257 13.2393 14.8276C12.7633 15.1734 12.2163 15.3189 11.5859 15.3872C10.9683 15.4541 10.1942 15.4536 9.23438 15.4536H6.76563C5.80591 15.4536 5.03168 15.4541 4.41407 15.3872C3.78385 15.3189 3.23665 15.1734 2.76074 14.8276C2.48291 14.6257 2.23802 14.3809 2.03614 14.103C1.69066 13.6272 1.54483 13.0798 1.47657 12.4497C1.40973 11.8321 1.41114 11.0576 1.41114 10.0981V5.90088C1.41113 4.94132 1.40966 4.16686 1.47657 3.54932C1.54488 2.91921 1.69042 2.37184 2.03614 1.896C2.2381 1.61807 2.4828 1.37333 2.76074 1.17139C3.23665 0.825682 3.78386 0.680109 4.41407 0.611819C5.03168 0.544905 5.80591 0.546389 6.76563 0.546389H9.23438ZM6.76563 1.896C5.77586 1.896 5.0876 1.89738 4.55957 1.95459C4.0443 2.01043 3.76214 2.11349 3.55469 2.26416C3.39135 2.38284 3.24761 2.52662 3.12891 2.68994C2.97821 2.89736 2.8752 3.17967 2.81934 3.69483C2.76214 4.22279 2.76075 4.91131 2.76074 5.90088V10.0981C2.76074 11.0876 2.76221 11.7762 2.81934 12.3042C2.87516 12.8194 2.97829 13.1026 3.12891 13.3101C3.24754 13.4733 3.39147 13.6172 3.55469 13.7358C3.76213 13.8865 4.04438 13.9896 4.55957 14.0454C5.0876 14.1026 5.77586 14.103 6.76563 14.103H9.23438C10.2242 14.103 10.9124 14.1026 11.4404 14.0454C11.9556 13.9896 12.2379 13.8865 12.4453 13.7358C12.6086 13.6172 12.7525 13.4733 12.8711 13.3101C13.0217 13.1026 13.1248 12.8195 13.1807 12.3042C13.2378 11.7762 13.2393 11.0876 13.2393 10.0981V5.90088C13.2393 4.91131 13.2379 4.22279 13.1807 3.69483C13.1248 3.17969 13.0218 2.89736 12.8711 2.68994C12.7524 2.52667 12.6086 2.38281 12.4453 2.26416C12.2379 2.11355 11.9556 2.01041 11.4404 1.95459C10.9124 1.8974 10.2241 1.896 9.23438 1.896H6.76563Z"
1801
- }
1802
- ];
1803
- function findNativeContextSvg() {
1804
- for (const ctx of document.querySelectorAll('[data-chat-flow-kind="context"]')) {
1805
- const drow = ctx.querySelector("[data-disclosure-row]");
1806
- if (drow === null) continue;
1807
- for (const svg of drow.querySelectorAll("svg")) {
1808
- if (svg.querySelectorAll("path").length >= 2) return svg;
279
+ reconcile(groups) {
280
+ const previousItems = /* @__PURE__ */ new Map();
281
+ for (const group of this.groups.values()) {
282
+ for (const id of group.items) previousItems.set(id, this.isExpanded(group.id));
1809
283
  }
1810
- }
1811
- return null;
1812
- }
1813
- function createContextIcon() {
1814
- const native = findNativeContextSvg();
1815
- if (native !== null) return native.cloneNode(true);
1816
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1817
- svg.setAttribute("viewBox", "0 0 16 16");
1818
- svg.setAttribute("width", "14");
1819
- svg.setAttribute("height", "14");
1820
- svg.setAttribute("fill", "none");
1821
- for (const p of CONTEXT_ICON_PATHS) {
1822
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1823
- path.setAttribute("d", p.d);
1824
- path.setAttribute("fill", "currentColor");
1825
- svg.appendChild(path);
1826
- }
1827
- return svg;
1828
- }
1829
- 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";
1830
- function findNativeWriteSvg() {
1831
- const selector = '[data-tool="write"] [data-disclosure-row], [data-tool="edit"] [data-disclosure-row]';
1832
- for (const drow of document.querySelectorAll(selector)) {
1833
- for (const svg of drow.querySelectorAll("svg")) {
1834
- if (svg.querySelectorAll("path").length === 1 && isIcon16(svg)) return svg;
284
+ const next = /* @__PURE__ */ new Map();
285
+ for (const group of groups) {
286
+ next.set(group.id, this.expanded.get(group.id) ?? group.items.some((id) => previousItems.get(id) === true));
1835
287
  }
288
+ this.groups = new Map(groups.map((group) => [group.id, group]));
289
+ this.expanded = next;
1836
290
  }
1837
- return null;
1838
- }
1839
- function isIcon16(svg) {
1840
- const vb = (svg.getAttribute("viewBox") ?? "").trim().split(/\s+/);
1841
- return vb.length === 4 && Number(vb[2]) === 16 && Number(vb[3]) === 16;
1842
- }
1843
- function createWriteIcon() {
1844
- const native = findNativeWriteSvg();
1845
- if (native !== null) return native.cloneNode(true);
1846
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1847
- svg.setAttribute("viewBox", "0 0 16 16");
1848
- svg.setAttribute("width", "14");
1849
- svg.setAttribute("height", "14");
1850
- svg.setAttribute("fill", "none");
1851
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1852
- path.setAttribute("d", WRITE_ICON_PATH);
1853
- path.setAttribute("fill", "currentColor");
1854
- svg.appendChild(path);
1855
- return svg;
1856
- }
1857
- function syncLeadingIcon(chip, kind) {
1858
- const leading = chip.querySelector(".dshcf-leading");
1859
- if (leading === null) return;
1860
- const existing = leading.querySelector("svg");
1861
- if (existing !== null && existing.getAttribute("data-dshcf-icon") === kind) return;
1862
- for (const child of [...leading.childNodes]) child.remove();
1863
- const svg = kind === "think" ? createThinkIcon() : kind === "context" ? createContextIcon() : kind === "write" ? createWriteIcon() : createCommandIcon();
1864
- svg.setAttribute("data-dshcf-icon", kind);
1865
- leading.appendChild(svg);
1866
- }
1867
- function findFlow() {
1868
- const flows = document.querySelectorAll("[data-chat-flow]");
1869
- for (const flow of flows) {
1870
- if (flow.offsetParent !== null || flow.getBoundingClientRect().width > 0) return flow;
291
+ isExpanded(id) {
292
+ return this.expanded.get(id) ?? false;
1871
293
  }
1872
- return flows[0] ?? null;
1873
- }
1874
- function nodeWithin(node, ancestor) {
1875
- for (let current = node; current !== null; current = current.parentNode) {
1876
- if (current === ancestor) return true;
294
+ setExpanded(id, value) {
295
+ if (this.groups.has(id)) this.expanded.set(id, value);
1877
296
  }
1878
- return false;
1879
- }
1880
- function isPluginOwnedNode(node) {
1881
- const element = node.nodeType === 1 ? node : node.parentElement;
1882
- return element !== null && element !== void 0 && element.closest(".dshcf-chip, .dshcf-processed, .dshcf-merged-think, .dshcf-merged-body") !== null;
1883
- }
1884
- function flowChildOwner(node, flow) {
1885
- let current = node;
1886
- while (current !== null && current.parentNode !== flow) current = current.parentNode;
1887
- return current instanceof HTMLElement ? current : null;
1888
- }
1889
- function flowItems(flow) {
1890
- return [...flow.children].filter((el) => el instanceof HTMLElement && !el.classList.contains("dshcf-processed") && !el.classList.contains("dshcf-flow-chip"));
1891
- }
1892
- function isDisplayed(el) {
1893
- if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
1894
- return el.style.display !== "none";
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 });
297
+ clear() {
298
+ this.groups.clear();
299
+ this.expanded.clear();
1909
300
  }
1910
- return states;
301
+ };
302
+
303
+ // src/summary.ts
304
+ function toolAction(tool) {
305
+ if (/^(write|edit|str_replace|apply_patch)/.test(tool)) return "\u7F16\u8F91\u4E86\u6587\u4EF6";
306
+ if (/^(read|glob|grep|list_directory|file_search)/.test(tool)) return "\u8BFB\u53D6\u4E86\u6587\u4EF6";
307
+ if (/^(web_search|search)/.test(tool)) return "\u641C\u7D22\u4E86\u8D44\u6599";
308
+ if (/(browser|browse|playwright|chrome|web_fetch)/.test(tool)) return "\u4F7F\u7528\u4E86\u6D4F\u89C8\u5668";
309
+ if (/^(bash|pwsh|shell|run_code|exec|cordis_run)/.test(tool)) return "\u8FD0\u884C\u4E86\u547D\u4EE4";
310
+ return tool === "" ? "\u8C03\u7528\u4E86\u5DE5\u5177" : `\u8C03\u7528\u4E86 ${tool}`;
1911
311
  }
1912
- function elementTurn(el) {
1913
- const turn = el.getAttribute("data-chat-turn");
1914
- return turn === null || turn === "" ? null : turn;
312
+ function normalizeStatus(value) {
313
+ if (value === "running") return "running";
314
+ if (value === "error" || value === "failed") return "error";
315
+ if (value === "stopped" || value === "interrupted" || value === "cancelled") return "stopped";
316
+ if (value === "waiting" || value === "pending" || value === "approval") return "waiting";
317
+ return "done";
1915
318
  }
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;
319
+ function summarize(infos) {
320
+ const last = infos[infos.length - 1];
321
+ const running = [...infos].reverse().find((info) => info.status === "running" || info.status === "waiting");
322
+ const current = running ?? last;
323
+ const actions = [...new Set(infos.map((info) => info.action))];
324
+ const status = running?.status ?? (infos.some((info) => info.status === "error") ? "error" : infos.some((info) => info.status === "stopped") ? "stopped" : "done");
325
+ let text = actions.join(" \xB7 ");
326
+ let detail = "";
327
+ if (running !== void 0) {
328
+ const active = running.status === "waiting" ? "\u7B49\u5F85\u64CD\u4F5C" : running.kind === "think" ? "\u6B63\u5728\u601D\u8003" : running.kind === "context" ? "\u6B63\u5728\u6CE8\u5165\u4E0A\u4E0B\u6587" : "\u6B63\u5728\u8FD0\u884C";
329
+ text = [...actions.filter((action) => action !== running.action), active].join(" \xB7 ");
330
+ detail = running.detail;
331
+ } else if (status === "error") text += " \xB7 \u51FA\u9519";
332
+ else if (status === "stopped") text += " \xB7 \u5DF2\u505C\u6B62";
333
+ return { text: text || "\u5DE5\u4F5C\u8FC7\u7A0B", detail, status, kind: current?.kind ?? "tool" };
1923
334
  }
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;
335
+
336
+ // src/work-info.ts
337
+ function preview(row) {
338
+ const follow = row.querySelector("[data-follow-end]");
339
+ if (follow !== null) return (follow.textContent ?? "").trim().slice(-240);
340
+ const disclosure = row.querySelector("[data-disclosure-row]");
341
+ if (disclosure === null) return "";
342
+ for (const child of [...disclosure.children].slice(2)) {
343
+ if (child.getAttribute("aria-hidden") === "true") continue;
344
+ const value = (child.textContent ?? "").trim();
345
+ if (value !== "") return value.replace(/\s+/g, " ").slice(0, 240);
346
+ }
347
+ return "";
1928
348
  }
1929
- function stableElementKey(el, fallbackIndex) {
1930
- const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
1931
- const key = el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`;
1932
- return `${kind}:${key}`;
349
+ function readWorkInfo(item) {
350
+ const root = item.kind === "tool" ? item.row.querySelector("[data-tool]") ?? item.row : item.kind === "command" ? item.row.querySelector("[data-state]") ?? item.row : item.row;
351
+ const action = item.kind === "think" ? "\u5DF2\u601D\u8003" : item.kind === "context" ? "\u5DF2\u6CE8\u5165\u4E0A\u4E0B\u6587" : item.kind === "command" ? "\u8FD0\u884C\u4E86\u547D\u4EE4" : toolAction((root.getAttribute("data-tool") ?? "").slice(0, 80));
352
+ return { kind: item.kind, action, detail: preview(item.row), status: normalizeStatus(root.getAttribute("data-state")) };
1933
353
  }
1934
- function hasLeadingTurnWork(items) {
1935
- return items.some((el) => {
1936
- const kind = el.getAttribute("data-chat-flow-kind");
1937
- return kind === "assistant-step" || kind === "assistant" || kind === "tool-call" || kind === "command" || kind === "manual-compaction";
1938
- });
354
+ function needsAttention(items, infos) {
355
+ return items.some(
356
+ (item) => item.cover.contains(document.activeElement) || item.row.hasAttribute("data-selected") || item.row.querySelector("[data-selected], input:not(:disabled), textarea:not(:disabled), select:not(:disabled)") !== null || infos.get(item.id)?.status === "waiting"
357
+ );
1939
358
  }
1940
- function buildSegments(flow, blocks, hasBody) {
1941
- const items = flowItems(flow);
1942
- const itemIndex = new Map(items.map((el, index) => [el, index]));
1943
- const snapshots = [];
1944
- let contentStart = 0;
1945
- let startMarker = null;
1946
- const append = (end, boundary, closed) => {
1947
- if (end < contentStart) return;
1948
- const range = items.slice(contentStart, end);
1949
- const inRange = new Set(range);
1950
- const segmentBlocks = blocks.filter((block) => inRange.has(block.host));
1951
- const bodySteps = range.filter((el) => {
1952
- const kind = el.getAttribute("data-chat-flow-kind");
1953
- return (kind === "assistant-step" || kind === "assistant") && hasBody(el);
1954
- });
1955
- const finalStep = bodySteps.length > 0 ? bodySteps[bodySteps.length - 1] : null;
1956
- const middleSteps = new Set(bodySteps.slice(0, -1));
1957
- const workHosts = /* @__PURE__ */ new Set([
1958
- ...segmentBlocks.map((block) => block.host),
1959
- ...middleSteps
1960
- ]);
1961
- const firstWork = range.find((el) => workHosts.has(el)) ?? finalStep;
1962
- const identity = startMarker ?? range.find((el) => hasLeadingTurnWork([el])) ?? boundary;
1963
- const identityIndex = identity === null ? contentStart : itemIndex.get(identity) ?? contentStart;
1964
- const prefix = startMarker === null ? "leading" : "segment";
1965
- const key = `${prefix}:${identity === null ? `open:${contentStart}` : stableElementKey(identity, identityIndex)}`;
1966
- snapshots.push({
1967
- key,
1968
- boundary,
1969
- startMarker,
1970
- blocks: segmentBlocks,
1971
- middleSteps,
1972
- finalStep,
1973
- firstWork,
1974
- closed,
1975
- running: segmentBlocks.some((block) => block.rows.some((row) => rowState(row) === "running")),
1976
- hasWork: segmentBlocks.length > 0 || middleSteps.size > 0
359
+
360
+ // src/styles.css
361
+ var styles_default = '/* Keep native content searchable while removing folded work from flex/grid spacing. */\n[data-dshcf-folded][hidden="until-found"] {\n position: absolute !important;\n content-visibility: hidden !important;\n inline-size: 0 !important;\n block-size: 0 !important;\n min-inline-size: 0 !important;\n min-block-size: 0 !important;\n margin: 0 !important;\n padding: 0 !important;\n border-width: 0 !important;\n overflow: clip !important;\n}\n.dshcf-group {\n appearance: none;\n display: flex;\n align-items: center;\n align-self: flex-start;\n gap: 6px;\n box-sizing: border-box;\n max-width: 100%;\n min-width: 0;\n min-height: 24px;\n margin: 0;\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #919191);\n font: 400 var(--dsh-content-font-size, 14px)/24px system-ui, sans-serif;\n text-align: left;\n cursor: pointer;\n}\n.dshcf-group[hidden] { display: none; }\n.dshcf-group:hover { color: var(--dsw-alias-label-primary, #d5d5d5); }\n.dshcf-group:focus-visible { outline: 2px solid var(--dsw-alias-state-focus-ring, #4d6bfe); outline-offset: 3px; border-radius: 3px; }\n.dshcf-group-icon { display: flex; flex: none; width: 16px; height: 16px; align-items: center; justify-content: center; }\n.dshcf-group-icon svg { display: block; width: 14px; height: 14px; }\n.dshcf-group-label { flex: 0 0 auto; min-width: 0; max-width: calc(100% - 40px); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n.dshcf-group[data-status="running"] .dshcf-group-label,\n.dshcf-group[data-status="waiting"] .dshcf-group-label { max-width: 65%; }\n.dshcf-group-detail { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #aaa); }\n.dshcf-group-detail:empty { display: none; }\n.dshcf-group-chevron { flex: none; width: 12px; height: 12px; opacity: .65; transform: rotate(-90deg); }\n.dshcf-group[aria-expanded="true"] .dshcf-group-chevron { transform: none; }\n.dshcf-group[data-status="running"] .dshcf-group-label { animation: dshcf-pulse 1.6s ease-in-out infinite; }\n.dshcf-group[data-status="error"] { color: var(--dsw-alias-state-error-primary, #e5484d); }\n.dshcf-group[data-status="stopped"], .dshcf-group[data-status="waiting"] { color: var(--dsw-alias-state-warning-primary, #c58a2c); }\n@keyframes dshcf-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }\n@media (prefers-reduced-motion: reduce) { .dshcf-group[data-status="running"] .dshcf-group-label { animation: none; } }\n';
362
+
363
+ // src/dom-view.ts
364
+ var GroupView = class {
365
+ constructor(flow, toggle) {
366
+ this.flow = flow;
367
+ this.toggle = toggle;
368
+ this.owner = `v2-${Math.random().toString(36).slice(2)}`;
369
+ this.rows = /* @__PURE__ */ new Map();
370
+ this.leases = /* @__PURE__ */ new Map();
371
+ this.writes = /* @__PURE__ */ new Map();
372
+ this.displays = /* @__PURE__ */ new Map();
373
+ this.style = document.createElement("style");
374
+ this.style.id = "dshcf-v2-style";
375
+ this.style.textContent = styles_default;
376
+ document.head.append(this.style);
377
+ }
378
+ writeHidden(element, value) {
379
+ if (element.getAttribute("hidden") === value) return;
380
+ this.writes.set(element, value);
381
+ if (value === null) element.removeAttribute("hidden");
382
+ else element.setAttribute("hidden", value);
383
+ }
384
+ ownMutation(record) {
385
+ return record.type === "attributes" && record.attributeName === "hidden" && record.target instanceof HTMLElement && this.writes.has(record.target) && this.writes.get(record.target) === record.target.getAttribute("hidden");
386
+ }
387
+ clearWrites() {
388
+ this.writes.clear();
389
+ }
390
+ detachedGroups(nodes) {
391
+ return nodes.flatMap((node) => {
392
+ if (!(node instanceof HTMLElement) || node.isConnected) return [];
393
+ const id = node.getAttribute("data-dshcf-group");
394
+ return id !== null && this.rows.get(id)?.button === node ? [id] : [];
1977
395
  });
1978
- };
1979
- for (let index = 0; index < items.length; index++) {
1980
- const el = items[index];
1981
- const kind = el.getAttribute("data-chat-flow-kind");
1982
- if (kind === "user" || kind === "steering") {
1983
- if (startMarker !== null) {
1984
- append(index, el, true);
1985
- contentStart = index + 1;
1986
- } else {
1987
- const leading = items.slice(contentStart, index);
1988
- if (hasLeadingTurnWork(leading)) {
1989
- append(index, el, true);
1990
- contentStart = index + 1;
1991
- }
1992
- }
1993
- startMarker = el;
1994
- continue;
1995
- }
1996
- if (kind === "turn-tail") {
1997
- append(index, el, true);
1998
- contentStart = index + 1;
1999
- startMarker = null;
2000
- }
2001
396
  }
2002
- if (contentStart < items.length) append(items.length, null, false);
2003
- return snapshots;
2004
- }
2005
- function hasVisibleSegmentWork(segment) {
2006
- const workHosts = /* @__PURE__ */ new Set([
2007
- ...segment.blocks.map((block) => block.host),
2008
- ...segment.middleSteps
2009
- ]);
2010
- if (segment.startMarker !== null) workHosts.add(segment.startMarker);
2011
- if (segment.finalStep !== null) workHosts.add(segment.finalStep);
2012
- return [...workHosts].some(isDisplayed);
2013
- }
2014
- function findBlocks(flow, hasBody) {
2015
- const blocks = [];
2016
- const children = flowItems(flow);
2017
- let run = null;
2018
- let carry = [];
2019
- let carryHost = null;
2020
- const makeBlock = (host, category) => {
2021
- const block = {
2022
- key: "",
2023
- host,
2024
- rows: [],
2025
- containers: [],
2026
- mount: "inside",
2027
- category
2028
- };
2029
- blocks.push(block);
2030
- return block;
2031
- };
2032
- const flushCarry = () => {
2033
- if (carry.length === 0 || carryHost === null) return;
2034
- let own = blocks.find((block) => block.host === carryHost && block.category === "work");
2035
- if (own === void 0) own = makeBlock(carryHost, "work");
2036
- own.rows.push(...carry);
2037
- carry = [];
2038
- carryHost = null;
2039
- };
2040
- for (const el of children) {
2041
- const kind = el.getAttribute("data-chat-flow-kind");
2042
- if (kind === TURN_PROCESS_KIND) continue;
2043
- if (kind === "user" || kind === "steering" || kind === "turn-tail") {
2044
- flushCarry();
2045
- run = null;
2046
- continue;
397
+ beginRead() {
398
+ this.displays.clear();
399
+ }
400
+ fold(element, group) {
401
+ if (nativeOwnsHidden(element)) {
402
+ this.release(element);
403
+ return;
2047
404
  }
2048
- const thinkRows = thinkRowsIn(el);
2049
- const workRows = [...callRowsIn(el), ...commandRowsIn(el)];
2050
- const isToolPile = workRows.length > 0;
2051
- const isContext = kind === "context";
2052
- const msgHasBody = !isToolPile ? hasBody(el) : false;
2053
- if (isContext) {
2054
- flushCarry();
2055
- if (run === null || run.category !== "context") run = makeBlock(el, "context");
2056
- run.rows.push(el);
2057
- run.mount = "before";
2058
- continue;
405
+ const existing = this.leases.get(element);
406
+ if (existing === void 0) {
407
+ if (element.hasAttribute("hidden")) return;
408
+ this.leases.set(element, { hidden: null, marker: element.getAttribute(FOLDED), group });
409
+ element.setAttribute(FOLDED, this.owner);
410
+ } else if (element.getAttribute(FOLDED) !== this.owner) {
411
+ this.leases.delete(element);
412
+ return;
2059
413
  }
2060
- if (isToolPile || thinkRows.length > 0 && !msgHasBody) {
2061
- if (run === null || run.category !== "work") run = makeBlock(el, "work");
2062
- if (carry.length > 0) {
2063
- run.rows.push(...carry);
2064
- carry = [];
2065
- carryHost = null;
414
+ this.writeHidden(element, "until-found");
415
+ }
416
+ release(element) {
417
+ const lease = this.leases.get(element);
418
+ if (lease === void 0) return;
419
+ this.leases.delete(element);
420
+ if (element.getAttribute(FOLDED) !== this.owner) return;
421
+ if (!nativeOwnsHidden(element) && element.getAttribute("hidden") === "until-found") this.writeHidden(element, lease.hidden);
422
+ if (lease.marker === null) element.removeAttribute(FOLDED);
423
+ else element.setAttribute(FOLDED, lease.marker);
424
+ }
425
+ externallyHidden(item) {
426
+ for (let element = item.row; element !== null && element !== this.flow; element = element.parentElement) {
427
+ let display = this.displays.get(element);
428
+ if (display === void 0) {
429
+ display = getComputedStyle(element).display;
430
+ this.displays.set(element, display);
2066
431
  }
2067
- run.rows.push(...thinkRows, ...workRows);
2068
- if (el !== run.host && !workRows.includes(el)) {
2069
- run.containers.push(el);
2070
- }
2071
- if (workRows.includes(el)) run.mount = "before";
2072
- } else if (el.hasAttribute("data-chat-anchor-key") && (thinkRows.length > 0 || msgHasBody) || msgHasBody && kind !== null) {
2073
- flushCarry();
2074
- if (thinkRows.length > 0) {
2075
- const segments = splitThinkByBody(el, thinkRows);
2076
- if (run === null || run.category !== "work") run = makeBlock(el, "work");
2077
- run.rows.push(...segments[0]);
2078
- carry = segments.slice(1).flat();
2079
- carryHost = el;
2080
- }
2081
- run = null;
2082
- } else if (kind !== null && kind !== "assistant-step" && kind !== "assistant") {
2083
- flushCarry();
2084
- run = null;
2085
- }
2086
- }
2087
- flushCarry();
2088
- const indexByHost = new Map(children.map((el, index) => [el, index]));
2089
- const counts = /* @__PURE__ */ new Map();
2090
- for (const block of blocks) {
2091
- block.mount = block.rows.includes(block.host) ? "before" : block.mount;
2092
- const base = `${stableElementKey(block.host, indexByHost.get(block.host) ?? 0)}:${block.category}`;
2093
- const ordinal = counts.get(base) ?? 0;
2094
- counts.set(base, ordinal + 1);
2095
- block.key = `${base}:block:${ordinal}`;
2096
- }
2097
- return blocks;
2098
- }
2099
- function splitThinkByBody(el, rows) {
2100
- const texts = [];
2101
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
2102
- let node;
2103
- while ((node = walker.nextNode()) !== null) {
2104
- if (node.data.trim() === "") continue;
2105
- const parent = node.parentElement;
2106
- if (parent !== null && parent.closest('[data-variant="think"], [data-chat-call-id], .dshcf-chip, .dshcf-merged-think, .dshcf-merged-body') !== null) continue;
2107
- texts.push(node);
2108
- }
2109
- const hasBetween = (a, b) => {
2110
- for (const t of texts) {
2111
- const posA = a.compareDocumentPosition(t);
2112
- if ((posA & Node.DOCUMENT_POSITION_FOLLOWING) === 0) continue;
2113
- const posB = b.compareDocumentPosition(t);
2114
- return (posB & Node.DOCUMENT_POSITION_PRECEDING) !== 0;
432
+ if (display === "none") return true;
433
+ if (element.hasAttribute("hidden") && !this.leases.has(element)) return true;
2115
434
  }
2116
435
  return false;
2117
- };
2118
- const segments = [];
2119
- let current = [];
2120
- for (let i = 0; i < rows.length; i++) {
2121
- current.push(rows[i]);
2122
- if (i + 1 < rows.length && hasBetween(rows[i], rows[i + 1])) {
2123
- segments.push(current);
2124
- current = [];
2125
- }
2126
- }
2127
- if (current.length > 0) segments.push(current);
2128
- return segments.length > 0 ? segments : [rows];
2129
- }
2130
- function hasBodyText(el) {
2131
- const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
2132
- let node;
2133
- while ((node = walker.nextNode()) !== null) {
2134
- if (node.data.trim() === "") continue;
2135
- const parent = node.parentElement;
2136
- if (parent !== null && parent.closest('[data-variant="think"], [data-chat-call-id], .dshcf-chip, .dshcf-merged-think, .dshcf-merged-body') !== null) continue;
2137
- return true;
2138
436
  }
2139
- return false;
2140
- }
2141
- function hasBodyContent(el) {
2142
- const kind = el.getAttribute("data-chat-flow-kind");
2143
- if (kind === "command" || kind === "manual-compaction") return false;
2144
- if (hasBodyText(el)) return true;
2145
- const excluded = '[data-variant="think"], [data-chat-call-id], [data-variant="others"][data-state], .dshcf-chip, .dshcf-merged-think, .dshcf-merged-body';
2146
- for (const media of el.querySelectorAll("img, video, audio, canvas")) {
2147
- if (media.closest(excluded) === null) return true;
437
+ create(id) {
438
+ const button = document.createElement("button");
439
+ button.type = "button";
440
+ button.className = "dshcf-group";
441
+ button.setAttribute("data-dshcf-group", id);
442
+ const icon = document.createElement("span");
443
+ icon.className = "dshcf-group-icon";
444
+ icon.setAttribute("aria-hidden", "true");
445
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
446
+ svg.setAttribute("viewBox", "0 0 16 16");
447
+ svg.setAttribute("fill", "none");
448
+ const path = document.createElementNS(svg.namespaceURI, "path");
449
+ path.setAttribute("d", "M3 3.5h10M3 8h10M3 12.5h10M1 3.5h.1M1 8h.1M1 12.5h.1");
450
+ path.setAttribute("stroke", "currentColor");
451
+ path.setAttribute("stroke-width", "1.3");
452
+ path.setAttribute("stroke-linecap", "round");
453
+ svg.append(path);
454
+ icon.append(svg);
455
+ const label = document.createElement("span");
456
+ label.className = "dshcf-group-label";
457
+ const detail = document.createElement("span");
458
+ detail.className = "dshcf-group-detail";
459
+ const chevron = document.createElementNS(svg.namespaceURI, "svg");
460
+ chevron.setAttribute("class", "dshcf-group-chevron");
461
+ chevron.setAttribute("viewBox", "0 0 12 12");
462
+ chevron.setAttribute("aria-hidden", "true");
463
+ const arrow = document.createElementNS(svg.namespaceURI, "path");
464
+ arrow.setAttribute("d", "m3 4.5 3 3 3-3");
465
+ arrow.setAttribute("fill", "none");
466
+ arrow.setAttribute("stroke", "currentColor");
467
+ chevron.append(arrow);
468
+ button.append(icon, label, detail, chevron);
469
+ button.addEventListener("click", () => {
470
+ if (this.rows.get(id)?.button === button) this.toggle(id);
471
+ });
472
+ const row = { button, label, detail, targets: /* @__PURE__ */ new Set() };
473
+ this.rows.set(id, row);
474
+ return row;
2148
475
  }
2149
- return false;
2150
- }
2151
- function thinkRowsIn(el) {
2152
- const rows = [];
2153
- for (const row of el.querySelectorAll('[data-variant="think"]:not([data-tool])')) {
2154
- if (row.closest("[data-chat-call-id]") !== null) continue;
2155
- if (row.closest("[data-subcalls]") !== null) continue;
2156
- rows.push(row);
476
+ summarize(id, summary, expanded) {
477
+ const row = this.rows.get(id);
478
+ if (row === void 0) return;
479
+ if (row.label.textContent !== summary.text) row.label.textContent = summary.text;
480
+ const detail = expanded ? "" : summary.detail;
481
+ if (row.detail.textContent !== detail) row.detail.textContent = detail;
482
+ const attributes = {
483
+ "aria-expanded": String(expanded),
484
+ "aria-label": `${expanded ? "\u6536\u8D77" : "\u5C55\u5F00"}\u5DE5\u4F5C\u8FC7\u7A0B\uFF1A${summary.text}${detail ? ` \xB7 ${detail}` : ""}`,
485
+ "data-status": summary.status,
486
+ title: `${summary.text}${detail ? ` \xB7 ${detail}` : ""}`
487
+ };
488
+ for (const [name2, value] of Object.entries(attributes)) if (row.button.getAttribute(name2) !== value) row.button.setAttribute(name2, value);
489
+ }
490
+ prepare(id, items, expanded, summary, turnCollapsed) {
491
+ const available = items.filter((item) => item.row.isConnected && !nativeHidden(item.row, this.flow) && !this.externallyHidden(item));
492
+ const hiddenByTurn = (item) => turnCollapsed && item.followNativeTurn;
493
+ const anchor = available.find((item) => !hiddenByTurn(item))?.cover;
494
+ const desired = new Set(available.filter((item) => !expanded || hiddenByTurn(item)).map((item) => item.cover));
495
+ return { id, expanded, summary, anchor, desired };
496
+ }
497
+ apply({ id, expanded, summary, anchor, desired }) {
498
+ const row = this.rows.get(id) ?? this.create(id);
499
+ row.button.hidden = anchor === void 0;
500
+ if (anchor !== void 0 && (row.button.parentNode !== anchor.parentNode || row.button.nextSibling !== anchor)) {
501
+ const focused = document.activeElement === row.button;
502
+ anchor.before(row.button);
503
+ if (focused) row.button.focus({ preventScroll: true });
504
+ }
505
+ for (const target of row.targets) if (!desired.has(target)) this.release(target);
506
+ for (const target of desired) this.fold(target, id);
507
+ row.targets = desired;
508
+ this.summarize(id, summary, expanded);
509
+ }
510
+ focus(id) {
511
+ this.rows.get(id)?.button.focus({ preventScroll: true });
512
+ }
513
+ remove(id) {
514
+ const row = this.rows.get(id);
515
+ if (row === void 0) return;
516
+ for (const target of row.targets) this.release(target);
517
+ row.button.remove();
518
+ this.rows.delete(id);
519
+ }
520
+ dispose() {
521
+ for (const id of [...this.rows.keys()]) this.remove(id);
522
+ for (const target of [...this.leases.keys()]) this.release(target);
523
+ this.style.remove();
524
+ this.writes.clear();
525
+ this.displays.clear();
2157
526
  }
2158
- return rows;
2159
- }
2160
- function callRowsIn(el) {
2161
- const rows = [];
2162
- for (const row of el.querySelectorAll("[data-chat-call-id]")) {
2163
- if (row.closest("[data-subcalls]") !== null) continue;
2164
- if (row.closest("[data-chat-call-id]") !== row) continue;
2165
- rows.push(row);
527
+ };
528
+
529
+ // src/scheduler.ts
530
+ var Scheduler = class {
531
+ constructor(run) {
532
+ this.run = run;
533
+ this.frame = 0;
534
+ this.timeout = 0;
535
+ this.disposed = false;
2166
536
  }
2167
- return rows;
2168
- }
2169
- function commandRowsIn(el) {
2170
- const kind = el.getAttribute("data-chat-flow-kind");
2171
- if (kind !== "command" && kind !== "manual-compaction") return [];
2172
- const rows = [];
2173
- for (const row of el.querySelectorAll('[data-variant="others"][data-state]')) {
2174
- const parent = row.parentElement?.closest('[data-variant="others"][data-state]');
2175
- if (parent !== null && parent !== void 0 && parent !== row) continue;
2176
- rows.push(row);
537
+ schedule() {
538
+ if (this.disposed || this.frame !== 0 || this.timeout !== 0) return;
539
+ const flush = () => {
540
+ if (this.frame !== 0) cancelAnimationFrame(this.frame);
541
+ if (this.timeout !== 0) clearTimeout(this.timeout);
542
+ this.frame = this.timeout = 0;
543
+ if (!this.disposed) this.run();
544
+ };
545
+ this.frame = requestAnimationFrame(flush);
546
+ this.timeout = window.setTimeout(flush, 60);
2177
547
  }
2178
- return rows.length > 0 ? rows : [el];
2179
- }
2180
- function deriveRowInfo(row) {
2181
- const isThink = row.matches('[data-variant="think"]') && !row.hasAttribute("data-tool");
2182
- if (isThink) {
2183
- return { kind: "think", label: "Think", summary: thinkSummary(row), state: row.getAttribute("data-state") ?? "ok" };
548
+ dispose() {
549
+ this.disposed = true;
550
+ if (this.frame !== 0) cancelAnimationFrame(this.frame);
551
+ if (this.timeout !== 0) clearTimeout(this.timeout);
552
+ this.frame = this.timeout = 0;
2184
553
  }
2185
- if (row.getAttribute("data-chat-flow-kind") === "context") {
2186
- return { kind: "tool", label: "\u4E0A\u4E0B\u6587\u6CE8\u5165", summary: toolSummary(row), state: "ok" };
554
+ };
555
+
556
+ // src/status-text.ts
557
+ var COPY = /Deep diving[.…]*|深度求索中\s*[.…]*/;
558
+ var StatusTextController = class {
559
+ constructor(read) {
560
+ this.read = read;
561
+ this.flow = null;
562
+ this.observer = null;
563
+ this.texts = /* @__PURE__ */ new Map();
564
+ this.scheduler = new Scheduler(() => this.update());
2187
565
  }
2188
- const commandSeat = row.closest('[data-chat-flow-kind="command"], [data-chat-flow-kind="manual-compaction"]');
2189
- if (commandSeat !== null) {
2190
- const commandKind = commandSeat.getAttribute("data-chat-flow-kind");
2191
- return {
2192
- kind: "tool",
2193
- label: commandKind === "manual-compaction" ? "Compact" : "Command",
2194
- summary: toolSummary(row),
2195
- state: row.getAttribute("data-state") ?? "ok"
2196
- };
566
+ setFlow(flow) {
567
+ if (flow === this.flow) return;
568
+ this.observer?.disconnect();
569
+ this.restore();
570
+ this.flow = flow;
571
+ if (flow === null) return;
572
+ this.observer = new MutationObserver((records) => {
573
+ if (records.some((record) => {
574
+ const element = record.target.nodeType === 1 ? record.target : record.target.parentElement;
575
+ const status = element?.closest('[role="status"]');
576
+ if (status?.closest("[data-chat-flow-kind]") != null) return false;
577
+ if (record.type === "characterData" && record.target instanceof Text) {
578
+ return status != null && this.texts.get(record.target)?.written !== record.target.data;
579
+ }
580
+ return record.type === "childList" && (status != null || [...record.addedNodes].some((node) => node instanceof Element && (node.matches('[role="status"]') || node.querySelector('[role="status"]') !== null)));
581
+ })) this.refresh();
582
+ });
583
+ this.observer.observe(flow, { subtree: true, childList: true, characterData: true });
584
+ this.refresh();
2197
585
  }
2198
- const root = row.querySelector("[data-tool]") ?? row;
2199
- const tool = root.getAttribute("data-tool") ?? "";
2200
- const state = root.getAttribute("data-state") ?? "ok";
2201
- const label = TOOL_LABELS[tool] ?? tool;
2202
- return { kind: "tool", label: label !== "" ? label : "Tool", summary: toolSummary(row), state };
2203
- }
2204
- function thinkSummary(row) {
2205
- const follow = row.querySelector("[data-follow-end]");
2206
- if (follow !== null) {
2207
- const text = (follow.textContent ?? "").trim();
2208
- if (text !== "") return text;
586
+ refresh() {
587
+ this.scheduler.schedule();
2209
588
  }
2210
- return summaryFallback(row);
2211
- }
2212
- function toolSummary(row) {
2213
- const drow = row.querySelector("[data-disclosure-row]");
2214
- if (drow !== null) {
2215
- const children = [...drow.children].filter((el) => el instanceof HTMLElement);
2216
- for (const child of children.slice(2)) {
2217
- const text = (child.textContent ?? "").trim();
2218
- if (text !== "") return text;
589
+ update() {
590
+ if (this.flow === null) return;
591
+ const replacement = this.read();
592
+ if (replacement === "" || replacement === void 0) {
593
+ this.restore();
594
+ return;
595
+ }
596
+ for (const text of this.texts.keys()) if (!text.isConnected) this.texts.delete(text);
597
+ for (const status of this.flow.querySelectorAll('[role="status"]')) {
598
+ if (status.closest("[data-chat-flow-kind]") !== null) continue;
599
+ const walker = document.createTreeWalker(status, NodeFilter.SHOW_TEXT);
600
+ let node;
601
+ while ((node = walker.nextNode()) !== null) {
602
+ const text = node;
603
+ const previous = this.texts.get(text);
604
+ const original = previous?.written === text.data ? previous.original : text.data;
605
+ if (!COPY.test(original)) continue;
606
+ const written = original.replace(COPY, () => replacement);
607
+ this.texts.set(text, { original, written });
608
+ if (text.data !== written) text.data = written;
609
+ }
2219
610
  }
2220
611
  }
2221
- return summaryFallback(row);
2222
- }
2223
- function summaryFallback(row) {
2224
- const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT);
2225
- let best = "";
2226
- let node;
2227
- while ((node = walker.nextNode()) !== null) {
2228
- if (node.parentElement?.closest('[data-open="true"]') !== null) continue;
2229
- const text = node.data.trim();
2230
- if (text === "") continue;
2231
- if (text.length > best.length) best = text;
2232
- }
2233
- return best;
2234
- }
2235
- function deriveBlockInfo(rows) {
2236
- const infos = rows.map(deriveRowInfo);
2237
- const runningTool = infos.find((i) => i.kind === "tool" && i.state === "running") ?? null;
2238
- const runningThink = infos.find((i) => i.kind === "think" && i.state === "running") ?? null;
2239
- const tools = [...new Set(infos.filter((i) => i.kind === "tool").map((i) => i.label))];
2240
- return {
2241
- runningTool,
2242
- runningThink,
2243
- tools,
2244
- hasError: infos.some((i) => i.state === "error"),
2245
- hasStopped: infos.some((i) => i.state === "stopped"),
2246
- allContext: infos.length > 0 && infos.every((i) => i.label === "\u4E0A\u4E0B\u6587\u6CE8\u5165")
2247
- };
2248
- }
2249
- function updateChip(chip, rows, expanded) {
2250
- const info = deriveBlockInfo(rows);
2251
- const title = chip.querySelector(".dshcf-chip-title");
2252
- const summary = chip.querySelector(".dshcf-chip-summary");
2253
- const sep = chip.querySelector(".dshcf-chip-sep");
2254
- if (title === null || summary === null) return;
2255
- const running = info.runningTool ?? info.runningThink;
2256
- const collapsed = !expanded;
2257
- let titleText;
2258
- let summaryText;
2259
- if (info.runningTool !== null) {
2260
- titleText = "\u6B63\u5728\u8FD0\u884C";
2261
- summaryText = collapsed ? info.runningTool.summary : "";
2262
- } else if (info.runningThink !== null) {
2263
- titleText = "\u6B63\u5728\u601D\u8003";
2264
- summaryText = collapsed ? info.runningThink.summary : "";
2265
- } else if (info.tools.length > 0) {
2266
- titleText = info.allContext ? "\u4E0A\u4E0B\u6587\u6CE8\u5165" : info.tools.some((tool) => tool === "Edit" || tool === "Write") ? "\u7F16\u8F91\u4E86\u6587\u4EF6" : "\u8FD0\u884C\u4E86\u547D\u4EE4";
2267
- summaryText = "";
2268
- } else {
2269
- titleText = "\u5DF2\u601D\u8003";
2270
- summaryText = "";
612
+ restore() {
613
+ for (const [text, record] of this.texts) if (text.data === record.written) text.data = record.original;
614
+ this.texts.clear();
2271
615
  }
2272
- let kind = running !== null ? running.kind : info.allContext ? "context" : info.tools.length > 0 ? "tool" : "think";
2273
- if (running === null && info.tools.some((tool) => tool === "Edit" || tool === "Write")) kind = "write";
2274
- if (title.textContent !== titleText) title.textContent = titleText;
2275
- if (summary.textContent !== summaryText) summary.textContent = summaryText;
2276
- if (sep !== null) {
2277
- const sepDisplay = summaryText === "" ? "none" : "";
2278
- if (sep.style.display !== sepDisplay) sep.style.display = sepDisplay;
616
+ dispose() {
617
+ this.observer?.disconnect();
618
+ this.scheduler.dispose();
619
+ this.restore();
620
+ this.flow = null;
2279
621
  }
2280
- if (running !== null) {
2281
- summary.scrollLeft = summary.scrollWidth - summary.clientWidth;
2282
- } else if (chip.classList.contains("running") && summary.scrollLeft !== 0) {
2283
- summary.scrollLeft = 0;
622
+ };
623
+
624
+ // src/controller.ts
625
+ var STRUCTURAL_ATTRIBUTES = /* @__PURE__ */ new Set(["data-chat-flow-key", "data-chat-anchor-key", "data-chat-flow-kind", "data-chat-turn", "data-variant", "data-tool"]);
626
+ var FoldController = class {
627
+ constructor(statusText) {
628
+ this.flow = null;
629
+ this.observer = null;
630
+ this.view = null;
631
+ this.model = new WorkModel();
632
+ this.state = new GroupState();
633
+ this.scheduler = new Scheduler(() => this.flush());
634
+ this.items = /* @__PURE__ */ new Map();
635
+ this.groups = /* @__PURE__ */ new Map();
636
+ this.infos = /* @__PURE__ */ new Map();
637
+ this.itemGroups = /* @__PURE__ */ new Map();
638
+ this.rowItems = /* @__PURE__ */ new WeakMap();
639
+ this.seatGroups = /* @__PURE__ */ new Map();
640
+ this.turnGroups = /* @__PURE__ */ new Map();
641
+ this.controls = /* @__PURE__ */ new Map();
642
+ this.dirtySeats = /* @__PURE__ */ new Set();
643
+ this.dirtyItems = /* @__PURE__ */ new Set();
644
+ this.dirtyGroups = /* @__PURE__ */ new Set();
645
+ this.modelDirty = true;
646
+ this.discover = true;
647
+ this.force = true;
648
+ this.started = false;
649
+ this.disposed = false;
650
+ this.lastError = "";
651
+ this.stats = { passes: 0, structures: 0, groupUpdates: 0, infoUpdates: 0, lastDurationMs: 0 };
652
+ this.ready = () => this.start();
653
+ this.visible = () => {
654
+ if (!document.hidden) {
655
+ this.discover = true;
656
+ this.refresh();
657
+ }
658
+ };
659
+ this.reveal = (event) => this.revealAt(event.target, event.type === "beforematch");
660
+ this.status = statusText === void 0 ? null : new StatusTextController(statusText);
2284
661
  }
2285
- const expandedAttr = String(expanded);
2286
- if (chip.getAttribute("aria-expanded") !== expandedAttr) {
2287
- chip.setAttribute("aria-expanded", expandedAttr);
662
+ start() {
663
+ if (this.started || this.disposed || typeof document === "undefined") return;
664
+ if (document.body === null) {
665
+ document.addEventListener("DOMContentLoaded", this.ready, { once: true });
666
+ return;
667
+ }
668
+ const owner = document;
669
+ if (owner.__dshAutoCollapseV2 !== this) owner.__dshAutoCollapseV2?.stop();
670
+ owner.__dshAutoCollapseV2 = this;
671
+ document.removeEventListener("DOMContentLoaded", this.ready);
672
+ this.started = true;
673
+ this.observer = new MutationObserver((records) => this.mutations(records));
674
+ this.observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: OBSERVED_ATTRIBUTES });
675
+ document.addEventListener("visibilitychange", this.visible);
676
+ this.scheduler.schedule();
2288
677
  }
2289
- if (chip.dataset.kind !== kind) {
2290
- chip.dataset.kind = kind;
2291
- syncLeadingIcon(chip, kind);
678
+ refresh() {
679
+ if (this.disposed) return;
680
+ this.force = this.modelDirty = true;
681
+ this.status?.refresh();
682
+ this.scheduler.schedule();
2292
683
  }
2293
- const tip = expanded ? "\u6536\u8D77\u8FD9\u4E9B\u5361\u7247" : "\u5C55\u5F00\u8FD9\u4E9B\u5361\u7247";
2294
- if (chip.title !== tip) chip.title = tip;
2295
- setClass(chip, "running", running !== null);
2296
- setClass(chip, "error", !running && info.hasError);
2297
- setClass(chip, "stopped", !running && info.hasStopped && !info.hasError);
2298
- }
2299
- function setClass(el, cls, on) {
2300
- if (el.classList.contains(cls) !== on) el.classList.toggle(cls, on);
2301
- }
2302
- function truncateSummary(text, max) {
2303
- const t = text.replace(/\s+/g, " ").trim();
2304
- return t.length > max ? t.slice(0, max) + "\u2026" : t;
2305
- }
2306
- function stripMarkdown(text) {
2307
- return text.replace(/\*\*/g, "").replace(/^#{1,3}\s+/, "").trim();
2308
- }
2309
- function isThinkRow(row) {
2310
- return row.matches('[data-variant="think"]') && !row.hasAttribute("data-tool");
2311
- }
2312
- var turnDurationCache = /* @__PURE__ */ new WeakMap();
2313
- function parseTurnDuration(boundary) {
2314
- const text = boundary.textContent ?? "";
2315
- const cached = turnDurationCache.get(boundary);
2316
- if (cached !== void 0 && cached.text === text) return cached.duration;
2317
- const duration = parseTurnDurationText(boundary, text);
2318
- turnDurationCache.set(boundary, { text, duration });
2319
- return duration;
2320
- }
2321
- function parseTurnDurationText(boundary, text) {
2322
- const m = text.match(/用时\s*(\d+)分(\d+)秒|用时\s*(\d+)秒/);
2323
- if (m !== null) {
2324
- if (m[1] !== void 0 && m[2] !== void 0) return Number(m[1]) * 6e4 + Number(m[2]) * 1e3;
2325
- if (m[3] !== void 0) return Number(m[3]) * 1e3;
2326
- return void 0;
684
+ refreshStatus() {
685
+ if (!this.disposed) this.status?.refresh();
2327
686
  }
2328
- const end = parseTimeText(text);
2329
- const start = findTurnStart(boundary);
2330
- if (end !== void 0 && start !== void 0 && end > start) return end - start;
2331
- return void 0;
2332
- }
2333
- function parseTimeText(text) {
2334
- const m = text.match(/(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})/);
2335
- if (m === null) return void 0;
2336
- const year = m[1] !== void 0 ? Number(m[1]) : (/* @__PURE__ */ new Date()).getFullYear();
2337
- const t = new Date(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5])).getTime();
2338
- return Number.isNaN(t) ? void 0 : t;
2339
- }
2340
- function findTurnStart(boundary) {
2341
- const flow = boundary.parentElement;
2342
- if (flow === null) return void 0;
2343
- let best = null;
2344
- for (const s of flow.querySelectorAll('[class*="timeStart"]')) {
2345
- const pos = s.compareDocumentPosition(boundary);
2346
- if ((pos & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || (pos & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0 || s === boundary) best = s;
2347
- else break;
687
+ /** Small diagnostics for regressions/performance, never rendered into the chat. */
688
+ diagnostics() {
689
+ return { ...this.stats, groups: this.groups.size, items: this.items.size };
2348
690
  }
2349
- if (best === null) return void 0;
2350
- return parseTimeText(best.textContent ?? "");
2351
- }
2352
- function rowState(row) {
2353
- if (row.matches('[data-variant="think"]') && !row.hasAttribute("data-tool")) {
2354
- return row.getAttribute("data-state") ?? "ok";
691
+ itemAt(node) {
692
+ for (let element = elementOf(node); element !== null && element !== this.flow; element = element.parentElement) {
693
+ const id = this.rowItems.get(element);
694
+ if (id !== void 0) return id;
695
+ }
696
+ return void 0;
2355
697
  }
2356
- const root = row.querySelector("[data-tool]") ?? row;
2357
- return root.getAttribute("data-state") ?? "ok";
2358
- }
2359
- function createProcessedRowElement(duration) {
2360
- const btn = document.createElement("button");
2361
- btn.type = "button";
2362
- btn.className = "dshcf-processed";
2363
- btn.setAttribute("aria-expanded", "false");
2364
- const text = document.createElement("span");
2365
- text.textContent = duration !== void 0 ? `\u5DF2\u5904\u7406 ${formatDuration(duration)}` : "\u5DF2\u5904\u7406";
2366
- const chevron = createChevronIcon("dshcf-processed-chevron");
2367
- btn.append(text, chevron);
2368
- btn.title = "\u5C55\u5F00\u5DE5\u4F5C\u8FC7\u7A0B";
2369
- return btn;
2370
- }
2371
- function formatDuration(ms) {
2372
- const s = Math.round(ms / 1e3);
2373
- if (s < 60) return `${s}\u79D2`;
2374
- const h = Math.floor(s / 3600);
2375
- const m = Math.floor(s % 3600 / 60);
2376
- const r = s % 60;
2377
- if (h > 0) {
2378
- return m > 0 ? `${h}\u5C0F\u65F6${m}\u5206` : `${h}\u5C0F\u65F6`;
698
+ dirtySeatGroups(seat) {
699
+ if (seat === null) return;
700
+ for (const id of this.seatGroups.get(seat) ?? []) this.dirtyGroups.add(id);
701
+ if (seat.getAttribute("data-chat-flow-kind") === "turn-process") {
702
+ const turn = seat.getAttribute("data-chat-turn");
703
+ if (turn !== null) for (const id of this.turnGroups.get(turn) ?? []) this.dirtyGroups.add(id);
704
+ }
2379
705
  }
2380
- if (r === 0) return `${m}\u5206`;
2381
- return `${m}\u5206${String(r).padStart(2, "0")}\u79D2`;
2382
- }
2383
- function injectStyle() {
2384
- if (document.getElementById(STYLE_ID) !== null) return;
2385
- const style = document.createElement("style");
2386
- style.id = STYLE_ID;
2387
- style.textContent = CHIP_CSS;
2388
- document.head.appendChild(style);
2389
- }
2390
- function replaceTurnStatus(flow, originals, statusText) {
2391
- const statuses = flow.matches('[role="status"]') ? [flow, ...flow.querySelectorAll('[role="status"]')] : [...flow.querySelectorAll('[role="status"]')];
2392
- for (const status of statuses) {
2393
- for (const node of status.childNodes) {
2394
- if (node instanceof Text && TURN_STATUS_COPY_RE.test(node.data)) {
2395
- let record = originals.get(node);
2396
- if (record === void 0) {
2397
- record = { original: node.data, written: "" };
2398
- originals.set(node, record);
706
+ mutations(records) {
707
+ if (this.disposed) return;
708
+ let relevant = false;
709
+ for (const record of records) {
710
+ if (pluginOwned(record.target) || this.view?.ownMutation(record)) continue;
711
+ const changed = [...record.addedNodes, ...record.removedNodes];
712
+ for (const id of this.view?.detachedGroups(changed) ?? []) {
713
+ this.dirtyGroups.add(id);
714
+ relevant = true;
715
+ }
716
+ if (record.type === "childList" && changed.length > 0 && changed.every(pluginOwned)) continue;
717
+ const flow = this.flow;
718
+ if (flow === null || !flow.isConnected) {
719
+ this.discover = relevant = true;
720
+ continue;
721
+ }
722
+ if (!flow.contains(record.target)) {
723
+ if (record.type === "attributes" && record.target instanceof Element && record.target.contains(flow)) {
724
+ this.discover = this.force = this.modelDirty = relevant = true;
725
+ }
726
+ if (record.type === "childList" && changed.some((node) => node instanceof Element && (node.matches("[data-chat-flow]") || node.querySelector("[data-chat-flow]") !== null))) this.discover = relevant = true;
727
+ continue;
728
+ }
729
+ const seat = seatOf(record.target, flow);
730
+ if (seat === null && record.target !== flow) continue;
731
+ relevant = true;
732
+ const item = this.itemAt(record.target);
733
+ if (record.type === "attributes" && STRUCTURAL_ATTRIBUTES.has(record.attributeName ?? "")) {
734
+ this.modelDirty = true;
735
+ if (seat !== null) this.dirtySeats.add(seat);
736
+ } else if (item !== void 0) {
737
+ this.dirtyItems.add(item);
738
+ if (record.type === "attributes") this.dirtySeatGroups(seat);
739
+ } else if (record.type === "attributes") {
740
+ this.dirtySeatGroups(seat);
741
+ if (seat === null) {
742
+ this.force = this.modelDirty = true;
743
+ this.discover = true;
2399
744
  }
2400
- if (node.data !== record.written) record.original = node.data;
2401
- const next = node.data.replace(TURN_STATUS_COPY_RE, statusText);
2402
- if (node.data !== next) {
2403
- node.data = next;
2404
- record.written = next;
745
+ } else {
746
+ this.modelDirty = true;
747
+ if (seat !== null) this.dirtySeats.add(seat);
748
+ this.dirtySeatGroups(seat);
749
+ }
750
+ }
751
+ this.view?.clearWrites();
752
+ if (relevant) this.scheduler.schedule();
753
+ }
754
+ switchFlow(flow) {
755
+ if (flow === this.flow) return;
756
+ this.flow?.removeEventListener("beforematch", this.reveal, true);
757
+ this.flow?.removeEventListener("focusin", this.reveal, true);
758
+ this.view?.dispose();
759
+ this.view = null;
760
+ this.model.clear();
761
+ this.state.clear();
762
+ this.items.clear();
763
+ this.groups.clear();
764
+ this.infos.clear();
765
+ this.itemGroups.clear();
766
+ this.seatGroups.clear();
767
+ this.turnGroups.clear();
768
+ this.controls.clear();
769
+ this.rowItems = /* @__PURE__ */ new WeakMap();
770
+ this.dirtySeats.clear();
771
+ this.dirtyItems.clear();
772
+ this.dirtyGroups.clear();
773
+ this.flow = flow;
774
+ this.force = this.modelDirty = true;
775
+ this.status?.setFlow(flow);
776
+ if (flow !== null) {
777
+ this.view = new GroupView(flow, (id) => this.toggle(id));
778
+ flow.addEventListener("beforematch", this.reveal, true);
779
+ flow.addEventListener("focusin", this.reveal, true);
780
+ }
781
+ }
782
+ rebuild(flow) {
783
+ const previousItems = this.items;
784
+ const previousGroups = this.groups;
785
+ const previousControls = this.controls;
786
+ const snapshot = this.model.read(flow, this.dirtySeats, this.force);
787
+ const groups = groupWork(snapshot.tokens);
788
+ this.state.reconcile(groups);
789
+ this.items = snapshot.items;
790
+ this.groups = new Map(groups.map((group) => [group.id, group]));
791
+ this.itemGroups.clear();
792
+ this.seatGroups.clear();
793
+ this.turnGroups.clear();
794
+ this.rowItems = /* @__PURE__ */ new WeakMap();
795
+ this.controls = nativeControls(flow);
796
+ for (const id of previousGroups.keys()) if (!this.groups.has(id)) this.view?.remove(id);
797
+ for (const id of this.infos.keys()) if (!this.items.has(id)) this.infos.delete(id);
798
+ for (const group of groups) {
799
+ const previous = previousGroups.get(group.id);
800
+ if (this.force || previous === void 0 || previous.items.length !== group.items.length || previous.items.some((id, i) => id !== group.items[i])) this.dirtyGroups.add(group.id);
801
+ if (group.turn !== null) {
802
+ if (previousControls.get(group.turn) !== this.controls.get(group.turn)) this.dirtyGroups.add(group.id);
803
+ const siblings = this.turnGroups.get(group.turn) ?? /* @__PURE__ */ new Set();
804
+ siblings.add(group.id);
805
+ this.turnGroups.set(group.turn, siblings);
806
+ }
807
+ for (const id of group.items) {
808
+ const item = this.items.get(id);
809
+ const previousItem = previousItems.get(id);
810
+ if (previousItem !== item) {
811
+ this.dirtyItems.add(id);
812
+ this.dirtyGroups.add(group.id);
2405
813
  }
814
+ this.itemGroups.set(id, group.id);
815
+ this.rowItems.set(item.row, id);
816
+ const owners = this.seatGroups.get(item.seat) ?? /* @__PURE__ */ new Set();
817
+ owners.add(group.id);
818
+ this.seatGroups.set(item.seat, owners);
2406
819
  }
2407
820
  }
821
+ this.dirtySeats.clear();
822
+ this.force = this.modelDirty = false;
823
+ this.stats.structures++;
2408
824
  }
2409
- }
2410
- function restoreTurnStatus(originals) {
2411
- for (const [node, record] of originals) {
2412
- if (node.isConnected && node.data === record.written && node.data !== record.original) node.data = record.original;
825
+ prepare(group) {
826
+ const items = group.items.map((id) => this.items.get(id)).filter(Boolean);
827
+ if (!this.state.isExpanded(group.id) && needsAttention(items, this.infos)) {
828
+ this.state.setExpanded(group.id, true);
829
+ if (group.turn !== null) openNative(this.controls.get(group.turn));
830
+ }
831
+ this.stats.groupUpdates++;
832
+ const turnCollapsed = group.turn !== null && nativeCollapsed(this.controls.get(group.turn));
833
+ return this.view?.prepare(group.id, items, this.state.isExpanded(group.id), summarize(group.items.map((id) => this.infos.get(id)).filter(Boolean)), turnCollapsed);
2413
834
  }
2414
- originals.clear();
2415
- }
2416
- function removeStyle() {
2417
- document.getElementById(STYLE_ID)?.remove();
2418
- }
835
+ flush() {
836
+ if (this.disposed) return;
837
+ const start = performance.now();
838
+ this.stats.passes++;
839
+ try {
840
+ if (this.discover || !this.flow?.isConnected) {
841
+ this.discover = false;
842
+ this.switchFlow(findFlow());
843
+ }
844
+ const flow = this.flow;
845
+ if (flow === null) return;
846
+ if (this.modelDirty) this.rebuild(flow);
847
+ this.view?.beginRead();
848
+ const summaries = /* @__PURE__ */ new Set();
849
+ for (const id of this.dirtyItems) {
850
+ const item = this.items.get(id);
851
+ if (item === void 0) continue;
852
+ this.infos.set(id, readWorkInfo(item));
853
+ this.stats.infoUpdates++;
854
+ const group = this.itemGroups.get(id);
855
+ if (group !== void 0) summaries.add(group);
856
+ }
857
+ const prepared = [];
858
+ for (const id of this.dirtyGroups) {
859
+ const group = this.groups.get(id);
860
+ if (group !== void 0) {
861
+ const update = this.prepare(group);
862
+ if (update !== void 0) prepared.push(update);
863
+ }
864
+ summaries.delete(id);
865
+ }
866
+ for (const update of prepared) this.view?.apply(update);
867
+ for (const id of summaries) {
868
+ const group = this.groups.get(id);
869
+ if (group !== void 0) this.view?.summarize(id, summarize(group.items.map((item) => this.infos.get(item)).filter(Boolean)), this.state.isExpanded(id));
870
+ }
871
+ this.lastError = "";
872
+ } catch (error) {
873
+ this.observer?.disconnect();
874
+ this.switchFlow(null);
875
+ if (!this.disposed && document.body !== null) this.observer?.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: OBSERVED_ATTRIBUTES });
876
+ const message = String(error);
877
+ if (this.lastError !== message) console.error("[dsh-auto-collapse] restored native view after an error", error);
878
+ this.lastError = message;
879
+ } finally {
880
+ this.dirtyGroups.clear();
881
+ this.dirtyItems.clear();
882
+ this.stats.lastDurationMs = performance.now() - start;
883
+ }
884
+ }
885
+ toggle(id) {
886
+ const group = this.groups.get(id);
887
+ if (group === void 0 || this.disposed) return;
888
+ const expanded = !this.state.isExpanded(id);
889
+ this.state.setExpanded(id, expanded);
890
+ if (expanded && group.turn !== null) {
891
+ openNative(this.controls.get(group.turn));
892
+ this.view?.focus(id);
893
+ }
894
+ this.dirtyGroups.add(id);
895
+ this.scheduler.schedule();
896
+ }
897
+ revealAt(node, search) {
898
+ if (this.flow === null || pluginOwned(node)) return;
899
+ const item = this.itemAt(node);
900
+ const groups = item !== void 0 ? [this.itemGroups.get(item)] : [...this.seatGroups.get(seatOf(node, this.flow)) ?? []];
901
+ for (const id of groups) {
902
+ const group = this.groups.get(id);
903
+ if (group === void 0) continue;
904
+ if (!search && this.state.isExpanded(id)) continue;
905
+ this.state.setExpanded(id, true);
906
+ if (group.turn !== null) openNative(this.controls.get(group.turn));
907
+ this.dirtyGroups.add(id);
908
+ }
909
+ if (groups.length > 0) this.scheduler.schedule();
910
+ }
911
+ stop() {
912
+ if (this.disposed) return;
913
+ this.disposed = true;
914
+ this.started = false;
915
+ this.observer?.disconnect();
916
+ this.scheduler.dispose();
917
+ if (typeof document !== "undefined") {
918
+ document.removeEventListener("DOMContentLoaded", this.ready);
919
+ document.removeEventListener("visibilitychange", this.visible);
920
+ }
921
+ this.switchFlow(null);
922
+ this.status?.dispose();
923
+ if (typeof document !== "undefined") {
924
+ const owner = document;
925
+ if (owner.__dshAutoCollapseV2 === this) delete owner.__dshAutoCollapseV2;
926
+ }
927
+ }
928
+ };
2419
929
 
2420
930
  // src/settings.ts
2421
931
  var AUTO_COLLAPSE_NS = "dsh-auto-collapse";
2422
- var DEFAULT_STATUS_TEXT2 = "Deep sleeping...";
932
+ var DEFAULT_STATUS_TEXT = "Deep sleeping...";
2423
933
  function statusTextProvider(scope) {
2424
934
  return () => {
2425
- if (scope === void 0) return DEFAULT_STATUS_TEXT2;
935
+ if (scope === void 0) return DEFAULT_STATUS_TEXT;
2426
936
  const snapshot = scope.getSnapshot();
2427
937
  const value = snapshot.value;
2428
- return value?.statusText ?? DEFAULT_STATUS_TEXT2;
938
+ return value?.statusText ?? DEFAULT_STATUS_TEXT;
2429
939
  };
2430
940
  }
2431
941
  var CARD_CSS = `
@@ -2520,14 +1030,26 @@ var __dshcfBundle = (() => {
2520
1030
  .dshcf-settings-discard:focus-visible,
2521
1031
  .dshcf-settings-save:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
2522
1032
  `;
2523
- var STYLE_ID2 = "dshcf-settings-style";
1033
+ var STYLE_ID = "dshcf-settings-style";
1034
+ var styleOwners = 0;
2524
1035
  function injectCardStyle() {
2525
- if (typeof document === "undefined" || document.getElementById(STYLE_ID2) !== null) return;
1036
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID) !== null) return;
2526
1037
  const style = document.createElement("style");
2527
- style.id = STYLE_ID2;
1038
+ style.id = STYLE_ID;
2528
1039
  style.textContent = CARD_CSS;
2529
1040
  document.head.appendChild(style);
2530
1041
  }
1042
+ function acquireCardStyle() {
1043
+ styleOwners += 1;
1044
+ injectCardStyle();
1045
+ let released = false;
1046
+ return () => {
1047
+ if (released) return;
1048
+ released = true;
1049
+ styleOwners = Math.max(0, styleOwners - 1);
1050
+ if (styleOwners === 0 && typeof document !== "undefined") document.getElementById(STYLE_ID)?.remove();
1051
+ };
1052
+ }
2531
1053
  function ChevronIcon(open) {
2532
1054
  const React = __require("react");
2533
1055
  const className = open ? "dshcf-settings-chevron dshcf-settings-chevronOpen" : "dshcf-settings-chevron";
@@ -2544,17 +1066,19 @@ var __dshcfBundle = (() => {
2544
1066
  const React = __require("react");
2545
1067
  const scope = props.scope;
2546
1068
  const [open, setOpen] = React.useState(false);
2547
- const [snapshot, setSnapshot] = React.useState(scope.getSnapshot());
1069
+ const snapshot = React.useSyncExternalStore(
1070
+ React.useCallback((listener) => scope.subscribe(listener), [scope]),
1071
+ React.useCallback(() => scope.getSnapshot(), [scope])
1072
+ );
2548
1073
  const [pending, setPending] = React.useState(null);
2549
1074
  const [saving, setSaving] = React.useState(false);
2550
1075
  const [failed, setFailed] = React.useState(false);
2551
- React.useEffect(() => scope.subscribe(() => setSnapshot(scope.getSnapshot())), [scope]);
2552
1076
  if (snapshot.status !== "ready") return null;
2553
1077
  const value = snapshot.value;
2554
1078
  const base = snapshot.base;
2555
1079
  const user = snapshot.user;
2556
1080
  const currentText = value?.statusText ?? "";
2557
- const defaultText = base?.statusText ?? DEFAULT_STATUS_TEXT2;
1081
+ const defaultText = base?.statusText ?? DEFAULT_STATUS_TEXT;
2558
1082
  const text = pending ? pending.text : currentText;
2559
1083
  const userHas = user !== void 0 && Object.prototype.hasOwnProperty.call(user, "statusText");
2560
1084
  const overridden = pending ? !pending.reset : userHas;
@@ -2573,20 +1097,21 @@ var __dshcfBundle = (() => {
2573
1097
  setFailed(false);
2574
1098
  };
2575
1099
  const save = async () => {
2576
- if (pending === null) return;
1100
+ if (pending === null || saving || !writable) return;
1101
+ const submitted = pending;
2577
1102
  setSaving(true);
2578
1103
  setFailed(false);
2579
1104
  try {
2580
1105
  if (pending.reset) await scope.unset("statusText");
2581
1106
  else await scope.set("statusText", pending.text.trim());
2582
- setPending(null);
1107
+ setPending((current) => current === submitted ? null : current);
2583
1108
  } catch {
2584
1109
  setFailed(true);
2585
1110
  } finally {
2586
1111
  setSaving(false);
2587
1112
  }
2588
1113
  };
2589
- const blocked = !dirty || saving;
1114
+ const blocked = !dirty || saving || !writable;
2590
1115
  const cardClass = `dshcf-settings-card${open ? " dshcf-settings-cardOpen" : ""}`;
2591
1116
  return React.createElement("li", { className: cardClass }, [
2592
1117
  React.createElement(
@@ -2614,7 +1139,7 @@ var __dshcfBundle = (() => {
2614
1139
  React.createElement("label", { className: "dshcf-settings-fieldLabel", htmlFor: "dshcf-status-text" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD"),
2615
1140
  overridden ? React.createElement("span", { className: "dshcf-settings-badges" }, [
2616
1141
  React.createElement("span", { className: "dshcf-settings-badge" }, "\u5DF2\u8986\u76D6"),
2617
- React.createElement("button", { type: "button", className: "dshcf-settings-reset", disabled: !writable, onClick: resetField }, "\u6062\u590D\u9ED8\u8BA4")
1142
+ React.createElement("button", { type: "button", className: "dshcf-settings-reset", disabled: !writable || saving, onClick: resetField }, "\u6062\u590D\u9ED8\u8BA4")
2618
1143
  ]) : null
2619
1144
  ]),
2620
1145
  React.createElement("input", {
@@ -2623,7 +1148,7 @@ var __dshcfBundle = (() => {
2623
1148
  type: "text",
2624
1149
  value: text,
2625
1150
  placeholder: "\u6DF1\u5EA6\u6C42\u7D22\u4E2D...",
2626
- disabled: !writable,
1151
+ disabled: !writable || saving,
2627
1152
  onChange: (event) => edit(event.target.value)
2628
1153
  }),
2629
1154
  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")
@@ -2637,35 +1162,67 @@ var __dshcfBundle = (() => {
2637
1162
  ]);
2638
1163
  }
2639
1164
  function setupSettingsCard(ctx, scope) {
2640
- injectCardStyle();
2641
- return ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
2642
- {
2643
- name: "settings.plugin.item",
2644
- key: AUTO_COLLAPSE_NS,
2645
- inject: () => ({ scope })
2646
- },
2647
- StatusTextCard
2648
- ));
1165
+ const releaseStyle = acquireCardStyle();
1166
+ try {
1167
+ const offSlot = ctx.slots.inject("settings.plugin.item", () => ctx.slots.register(
1168
+ {
1169
+ name: "settings.plugin.item",
1170
+ key: AUTO_COLLAPSE_NS,
1171
+ inject: () => ({ scope })
1172
+ },
1173
+ StatusTextCard
1174
+ ));
1175
+ return () => {
1176
+ try {
1177
+ offSlot();
1178
+ } finally {
1179
+ releaseStyle();
1180
+ }
1181
+ };
1182
+ } catch (error) {
1183
+ releaseStyle();
1184
+ throw error;
1185
+ }
2649
1186
  }
2650
1187
 
2651
1188
  // src/client.ts
2652
1189
  var name = "dsh-auto-collapse";
2653
1190
  var inject = [];
2654
1191
  function apply(ctx) {
1192
+ const fallbackStatusText = () => DEFAULT_STATUS_TEXT;
1193
+ let readStatusText = fallbackStatusText;
1194
+ const controller = new FoldController(() => readStatusText());
2655
1195
  ctx.effect(() => {
2656
- const settingsScope = ctx.get?.("settingsScope");
2657
- const slots = ctx.get?.("slots");
2658
- const scope = settingsScope?.bind({ namespace: AUTO_COLLAPSE_NS });
2659
- const controller = new FoldController(statusTextProvider(scope));
2660
1196
  controller.start();
2661
- const offScope = scope?.subscribe(() => controller.refresh());
2662
- const offSettings = slots === void 0 || scope === void 0 ? void 0 : setupSettingsCard({ slots }, scope);
2663
- return () => {
2664
- offScope?.();
2665
- offSettings?.();
2666
- controller.stop();
2667
- };
2668
- }, "dsh-auto-collapse: fold observer + settings card");
1197
+ return () => controller.stop();
1198
+ }, "dsh-auto-collapse: fold observer");
1199
+ ctx.inject?.(["settingsScope", "slots"], (serviceCtx) => {
1200
+ serviceCtx.effect(() => {
1201
+ const settingsScope = serviceCtx.get?.("settingsScope");
1202
+ const slots = serviceCtx.get?.("slots");
1203
+ if (settingsScope === void 0 || slots === void 0) return;
1204
+ const scope = settingsScope.bind({ namespace: AUTO_COLLAPSE_NS });
1205
+ const scopedStatusText = statusTextProvider(scope);
1206
+ const offScope = scope.subscribe(() => controller.refreshStatus());
1207
+ let offSettings;
1208
+ try {
1209
+ offSettings = setupSettingsCard({ slots }, scope);
1210
+ } catch (error) {
1211
+ offScope();
1212
+ throw error;
1213
+ }
1214
+ readStatusText = scopedStatusText;
1215
+ controller.refreshStatus();
1216
+ return () => {
1217
+ offScope();
1218
+ offSettings();
1219
+ if (readStatusText === scopedStatusText) {
1220
+ readStatusText = fallbackStatusText;
1221
+ controller.refreshStatus();
1222
+ }
1223
+ };
1224
+ }, "dsh-auto-collapse: settings scope + plugin card");
1225
+ });
2669
1226
  }
2670
1227
  return __toCommonJS(client_exports);
2671
1228
  })();