dsh-activity-pane 0.1.0

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/src/client.mjs ADDED
@@ -0,0 +1,3269 @@
1
+ // dsh-activity-pane 浏览器运行时。
2
+ //
3
+ // 挂载策略:把窗格作为 AppFrame 中 `conversation` 槽座的前置兄弟列插入
4
+ // (`#root [data-slot="conversation"] || .parentElement` 即 flex 行),让外壳的
5
+ // 让步链挤压中间栏;窄屏(<=767px)转为固定抽屉 + 浮动开关按钮。
6
+ //
7
+ // 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+ native
8
+ // `sessions.history` / `sessions.models` 冷会话读取 + 运行中会话的原生订阅
9
+ //(binding().session)+ 可选 `modelDirectories` 目录 store 订阅(模型选择实时更新,
10
+ // 缺失时回落一次性读取),不依赖任何第三方插件数据路由,也不做状态轮询。
11
+
12
+ const name = "dsh-activity-pane";
13
+ const inject = ["connection", "sessions", "workspaces"];
14
+
15
+ const CONVERSATION_SELECTOR = "#root [data-slot=\"conversation\"]";
16
+ const PANE_ATTR = "data-dsh-activity-pane";
17
+ const PANE_CLASS = "dap-pane";
18
+ const LIST_CLASS = "dap-list";
19
+ const RECENT_CLASS = "dap-recent";
20
+ const CARD_CLASS = "dap-card";
21
+ const STYLE_ID = "dsh-activity-pane-style";
22
+ const INSTANCE_KEY = "__dshActivityPaneCleanup";
23
+ /** 拖拽调宽的 localStorage 持久化键(R-01-015/AC-04)。 */
24
+ const WIDTH_STORAGE_KEY = "dsh-activity-pane:width";
25
+ const COLLAPSED_WIDTH = 34;
26
+ /** 宿主侧完成确认 API 前缀(C-030):acks 快照 / SSE 推送 / ack 写回,同源受信。 */
27
+ const ACK_API_BASE = "/dsh-activity-pane/api";
28
+ // 缩进槽宽:与连接线 CSS 几何耦合(left:-8px = INDENT_PX/2 缩进槽中线,
29
+ // top:-6px/bottom:-2px 对应 .dap-list 的 gap:6px),改任一数值须三处同步;
30
+ // scripts/check.mjs 有钉住断言。
31
+ const INDENT_PX = 16;
32
+ const MOBILE_BREAKPOINT = "767px";
33
+ /** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
34
+ const CLOCK_MS = 1000;
35
+ /** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
36
+ const LOAD_CONCURRENCY = 3;
37
+ /** 「回到顶部」悬浮按钮显隐阈值:scrollTop 超过该值(px)时显示(R-01-018/AC-01)。 */
38
+ const TOP_THRESHOLD = 200;
39
+
40
+ const CSS = `
41
+ [data-dsh-activity-pane] {
42
+ --dap-width: ${PANE_WIDTH_DEFAULT}px;
43
+ flex: 0 0 var(--dap-width);
44
+ min-width: 0;
45
+ min-height: 0;
46
+ position: relative;
47
+ z-index: 5;
48
+ display: flex;
49
+ flex-direction: column;
50
+ overflow: hidden;
51
+ box-sizing: border-box;
52
+ border-right: 1px solid color-mix(in srgb, currentColor 12%, transparent);
53
+ background: color-mix(in srgb, currentColor 3%, transparent);
54
+ color: var(--dsw-alias-label-primary, #e8ebf2);
55
+ user-select: none;
56
+ }
57
+ [data-dsh-activity-pane] .dap-header {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ padding: 10px 12px;
62
+ font-size: 12px;
63
+ font-weight: 700;
64
+ letter-spacing: 0.02em;
65
+ }
66
+ /* 标题行整体即收起控件,两端断点一致(R-01-011/AC-03、R-01-008/AC-02):指针与悬停/聚焦反馈。 */
67
+ [data-dsh-activity-pane] .dap-header { cursor: pointer; }
68
+ [data-dsh-activity-pane] .dap-header:hover,
69
+ [data-dsh-activity-pane] .dap-header:focus-visible {
70
+ background: color-mix(in srgb, currentColor 8%, transparent);
71
+ }
72
+ [data-dsh-activity-pane] .dap-header:focus-visible { outline: none; }
73
+ /* 方向符号:标题行的一部分而非独立按钮,两端断点一致呈现(R-01-011/AC-03、R-01-008/AC-02)。 */
74
+ [data-dsh-activity-pane] .dap-collapse-hint {
75
+ margin-left: auto;
76
+ color: color-mix(in srgb, currentColor 45%, transparent);
77
+ font-size: 13px;
78
+ }
79
+ [data-dsh-activity-pane] .dap-count {
80
+ flex: none;
81
+ font-size: 10px;
82
+ line-height: 16px;
83
+ font-weight: 600;
84
+ color: color-mix(in srgb, currentColor 88%, transparent);
85
+ background: color-mix(in srgb, currentColor 12%, transparent);
86
+ border-radius: 999px;
87
+ padding: 0 7px;
88
+ }
89
+ [data-dsh-activity-pane] .dap-count[data-awaiting] {
90
+ /* 底色/透明度与等待卡完全一致、无描边与外环(R-01-002/AC-06);脉冲走亮度呼吸而非整体
91
+ 不透明度——半透明会让底色透进列头背景;固定 1.2s 并与等待卡末行同步(AC-07、C-065)。
92
+ 任一等待行动即脉冲:三类等待行动行为一致(C-037)。
93
+ 底色跟随等待构成(C-040、C-043):默认金(阻塞在即),tone=done 取完成提醒同款暗绿、
94
+ tone=error 取错误提醒同款暗红(错误 > 阻塞 > 完成 优先级)。 */
95
+ background: rgba(46, 42, 26, 0.97);
96
+ animation: dap-await-pulse 1.2s ease-in-out infinite;
97
+ }
98
+ [data-dsh-activity-pane] .dap-count[data-awaiting][data-tone="done"],
99
+ [data-dsh-activity-pane] .dap-rail-count[data-awaiting][data-tone="done"],
100
+ .dap-toggle[data-awaiting][data-tone="done"] .dap-toggle-count {
101
+ background: rgba(32, 41, 35, 0.97);
102
+ }
103
+ [data-dsh-activity-pane] .dap-count[data-awaiting][data-tone="error"],
104
+ [data-dsh-activity-pane] .dap-rail-count[data-awaiting][data-tone="error"],
105
+ .dap-toggle[data-awaiting][data-tone="error"] .dap-toggle-count {
106
+ background: rgba(40, 29, 31, 0.97);
107
+ }
108
+ @keyframes dap-await-pulse { 0%,100% { filter: brightness(1); } 50% { filter: brightness(1.3); } }
109
+ /* 单一滚动区:活动区与最近历史同一容器滚动;touch-action/overscroll 防止
110
+ 触屏滚动穿透到下层页面(移动端「滚的是下面的会话界面」即根因)。 */
111
+ [data-dsh-activity-pane] .dap-scroll {
112
+ flex: 1;
113
+ min-height: 0;
114
+ overflow-y: auto;
115
+ overflow-x: hidden;
116
+ overscroll-behavior: contain;
117
+ touch-action: pan-y;
118
+ -webkit-overflow-scrolling: touch;
119
+ padding: 0 0 10px;
120
+ }
121
+ /* 滚动条仅在滚动时显示(R-01-004/AC-03,与外壳侧栏一致):thumb 默认透明,滚动中
122
+ 经 data-scrolling 显示。Firefox 路径必须在 @supports 门内——非 auto 的
123
+ scrollbar-color 会让 Chromium 丢弃该元素的 ::-webkit-scrollbar 规则。 */
124
+ [data-dsh-activity-pane] .dap-scroll::-webkit-scrollbar-thumb {
125
+ background: transparent;
126
+ }
127
+ [data-dsh-activity-pane] .dap-scroll[data-scrolling]::-webkit-scrollbar-thumb {
128
+ background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
129
+ }
130
+ @supports not selector(::-webkit-scrollbar) {
131
+ [data-dsh-activity-pane] .dap-scroll { scrollbar-color: transparent transparent; }
132
+ [data-dsh-activity-pane] .dap-scroll[data-scrolling] {
133
+ scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
134
+ }
135
+ }
136
+ /* 「回到顶部」悬浮图标按钮(R-01-018):窗格内右下角圆形按钮,纯图标无文字、不透明底色;
137
+ 默认 hidden,scrollTop 超阈值时由滚动监听揭隐。基类 display:flex 会压过 UA 的
138
+ [hidden] 规则,故显式补 [hidden] 隐藏。 */
139
+ [data-dsh-activity-pane] .dap-top {
140
+ position: absolute;
141
+ bottom: 12px;
142
+ right: 12px;
143
+ z-index: 6;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ width: 28px;
148
+ height: 28px;
149
+ padding: 0;
150
+ border: 1px solid rgba(255, 255, 255, 0.14);
151
+ border-radius: 999px;
152
+ background: #1d1f25;
153
+ color: inherit;
154
+ cursor: pointer;
155
+ }
156
+ [data-dsh-activity-pane] .dap-top[hidden] { display: none; }
157
+ [data-dsh-activity-pane] .dap-top:hover,
158
+ [data-dsh-activity-pane] .dap-top:focus-visible {
159
+ background: #262932;
160
+ }
161
+ [data-dsh-activity-pane] .dap-list {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 6px;
165
+ padding: 0 8px;
166
+ /* 轨道层的定位包含块(轨道绝对定位在列表坐标系内) */
167
+ position: relative;
168
+ }
169
+ /* 最近历史区:同一滚动区内的块段;整段可隐藏。 */
170
+ [data-dsh-activity-pane] .dap-recent {
171
+ flex: none;
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 6px;
175
+ border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
176
+ padding: 10px 8px 0;
177
+ margin-top: 10px;
178
+ }
179
+ [data-dsh-activity-pane] .dap-recent[hidden] { display: none; }
180
+ [data-dsh-activity-pane] .dap-recent-head {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 6px;
184
+ padding: 2px 2px 0;
185
+ font-size: 10px;
186
+ font-weight: 700;
187
+ letter-spacing: 0.06em;
188
+ color: color-mix(in srgb, currentColor 52%, transparent);
189
+ text-transform: uppercase;
190
+ }
191
+ /* 折叠:仅桌面生效;窄条 + 竖排标题与计数(R-01-011/AC-04)。 */
192
+ [data-dsh-activity-pane] .dap-rail {
193
+ display: none;
194
+ /* 窄条整体(含下方空白)均为展开命中区(R-01-011/AC-04):撑满窗格高度、内容顶对齐。 */
195
+ flex: 1;
196
+ flex-direction: column;
197
+ align-items: center;
198
+ gap: 8px;
199
+ width: 100%;
200
+ margin: 0;
201
+ padding: 10px 0 0;
202
+ border: 0;
203
+ background: none;
204
+ color: inherit;
205
+ font: inherit;
206
+ }
207
+ [data-dsh-activity-pane] .dap-rail-count {
208
+ min-width: 20px;
209
+ height: 20px;
210
+ border-radius: 999px;
211
+ padding: 0 5px;
212
+ text-align: center;
213
+ line-height: 20px;
214
+ font-size: 10px;
215
+ font-weight: 700;
216
+ background: color-mix(in srgb, currentColor 16%, transparent);
217
+ }
218
+ [data-dsh-activity-pane] .dap-rail-title {
219
+ writing-mode: vertical-rl;
220
+ font-size: 11px;
221
+ font-weight: 700;
222
+ letter-spacing: 0.1em;
223
+ color: color-mix(in srgb, currentColor 60%, transparent);
224
+ }
225
+ [data-dsh-activity-pane] .dap-rail-count[data-awaiting] {
226
+ background: rgba(46, 42, 26, 0.97);
227
+ animation: dap-await-pulse 1.2s ease-in-out infinite;
228
+ }
229
+ /* 桌面拖拽调宽手柄(R-01-015):右缘 6px 命中区,拖拽实时写入 --dap-width;
230
+ 折叠窄条与移动端抽屉不提供拖拽(下方两处媒体查询隐藏)。 */
231
+ [data-dsh-activity-pane] .dap-resize {
232
+ position: absolute;
233
+ top: 0; right: 0; bottom: 0;
234
+ width: 6px;
235
+ cursor: col-resize;
236
+ touch-action: none;
237
+ z-index: 6;
238
+ }
239
+ [data-dsh-activity-pane] .dap-resize:hover,
240
+ [data-dsh-activity-pane] .dap-resize[data-dragging] {
241
+ background: color-mix(in srgb, currentColor 18%, transparent);
242
+ }
243
+ /* 桌面:窗格作为中间列内的真实 flex 行元素参与布局——中间列被置为行方向,
244
+ 窗格固定宽、会话区弹性收缩,整个会话内容被真实挤到右边;折叠时收窄为窄条。 */
245
+ @media (min-width: 768px) {
246
+ [data-dsh-activity-pane][data-collapsed="true"] { flex-basis: ${COLLAPSED_WIDTH}px; }
247
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-header,
248
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-scroll { display: none; }
249
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-resize { display: none; }
250
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-top { display: none; }
251
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-rail { display: flex; cursor: pointer; }
252
+ /* 与展开态标题行对等的可点反馈(R-01-011/AC-04):悬停/聚焦高亮。 */
253
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-rail:hover,
254
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-rail:focus-visible {
255
+ background: color-mix(in srgb, currentColor 8%, transparent);
256
+ }
257
+ [data-dsh-activity-pane][data-collapsed="true"] .dap-rail:focus-visible { outline: none; }
258
+ }
259
+ @media (max-width: ${MOBILE_BREAKPOINT}) {
260
+ [data-dsh-activity-pane] .dap-resize { display: none; }
261
+ }
262
+ /* 卡片视觉沿用 answer-pet 的多会话卡片设计(MIT 参考,见 README)。 */
263
+ [data-dsh-activity-pane] .dap-card {
264
+ position: relative;
265
+ flex: none;
266
+ min-width: 0;
267
+ padding: 9px 11px;
268
+ border-radius: 14px;
269
+ background: rgba(29, 31, 37, 0.94);
270
+ border: 1px solid rgba(255, 255, 255, 0.13);
271
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
272
+ display: grid;
273
+ gap: 4px;
274
+ cursor: pointer;
275
+ }
276
+ /* 子代理层级连接线(R-01-003/AC-04):竖轨与横线全部由轨道层整体绘制——
277
+ syncTracks 测量各卡片浮点矩形,trackBoxes 统一取整到 CSS 像素后写入:
278
+ 每个母会话一条连续竖轨 .dap-conn-track(母会话底缘 → 末级子卡中心,
279
+ 含收口行),每个子卡一条横线 .dap-conn-stub(竖轨右缘 → 子卡左缘)。
280
+ 不分段拼接(接缝在随机亚像素相位下断口与重叠并存),也不让 CSS 按小数
281
+ 坐标定位任何线段(抗锯齿随机摊薄导致粗细不一、端点方头错位);统一取整
282
+ 使所有线段同相位,且轨道层整体 transform 对齐设备像素网格(层原点随窗
283
+ 口/滚动停在任意小数相位,不对齐则 1px 线段粗细不稳),滚动时经 rAF 重
284
+ 对齐(T-033)。 */
285
+ [data-dsh-activity-pane] .dap-tracks {
286
+ position: absolute;
287
+ inset: 0;
288
+ pointer-events: none;
289
+ }
290
+ [data-dsh-activity-pane] .dap-conn-track {
291
+ position: absolute;
292
+ width: 1px;
293
+ background: color-mix(in srgb, currentColor 24%, transparent);
294
+ pointer-events: none;
295
+ }
296
+ [data-dsh-activity-pane] .dap-conn-stub {
297
+ position: absolute;
298
+ height: 1px;
299
+ background: color-mix(in srgb, currentColor 24%, transparent);
300
+ pointer-events: none;
301
+ }
302
+ [data-dsh-activity-pane] .dap-card:hover {
303
+ border-color: rgba(255, 255, 255, 0.28);
304
+ }
305
+ [data-dsh-activity-pane] .dap-card:focus-visible {
306
+ outline: 2px solid color-mix(in srgb, currentColor 70%, transparent);
307
+ outline-offset: 2px;
308
+ }
309
+ [data-dsh-activity-pane] .dap-card[data-kind="subagent"] {
310
+ padding: 6px 10px;
311
+ border-radius: 12px;
312
+ background: rgba(25, 27, 32, 0.95);
313
+ }
314
+ [data-dsh-activity-pane] .dap-card[data-kind="recent"] {
315
+ padding: 6px 10px;
316
+ border-radius: 12px;
317
+ /* 弱化但可辨(R-01-013/AC-10、AC-11):opacity 0.8 承载淡化;底色介于窗格底色与
318
+ 活动卡(rgba(29,31,37,0.94))之间——暗于活动卡不抢视线、亮于背景可分辨,
319
+ 弱描边勾勒边界,淡化不得使卡片轮廓不可辨。 */
320
+ background: rgba(26, 28, 34, 0.92);
321
+ border-color: rgba(255, 255, 255, 0.08);
322
+ opacity: 0.8;
323
+ }
324
+ [data-dsh-activity-pane] .dap-card[data-current] {
325
+ border-color: color-mix(in srgb, #65a0ff 75%, transparent);
326
+ box-shadow: 0 0 0 1px color-mix(in srgb, #65a0ff 45%, transparent), 0 0 12px color-mix(in srgb, #65a0ff 30%, transparent);
327
+ }
328
+ /* 等待三类卡面按色彩语义分野(R-01-002/AC-03、AC-04、AC-13,C-040、C-043):阻塞等待
329
+ 采用金色系催促尽快响应(自琥珀 #e8a33d 调亮调纯为金黄 #f5c542,底色同步提亮提黄——
330
+ 与旧琥珀底差异须一眼可辨,东家视觉反馈);完成提醒采用绿色成功
331
+ 色系——深色取宿主 success 三级背景同款暗绿(green-900),底色仅保留轻微绿色色偏;
332
+ 错误提醒采用红色错误色系(与时间线错误分组行同源 #f06a72),警示会话出错。
333
+ --dap-wait-color 供状态点与末行胶囊共用的类别色相。 */
334
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="blocked"],
335
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="done"],
336
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="error"] {
337
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--dap-wait-color) 35%, transparent), 0 6px 16px rgba(0,0,0,.3);
338
+ }
339
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="blocked"] {
340
+ --dap-wait-color: #f5c542;
341
+ border-color: color-mix(in srgb, #f5c542 55%, transparent);
342
+ /* 金黄底:r/g 通道接近(明度较琥珀底更亮)、b 通道明显压低,扫一眼即金黄而非橙棕。 */
343
+ background: rgba(46, 42, 26, 0.97);
344
+ }
345
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="done"] {
346
+ --dap-wait-color: #58c98f;
347
+ border-color: color-mix(in srgb, #58c98f 55%, transparent);
348
+ /* 底色仅保留轻微绿色色偏(g 通道高于 r/b 均值约 +7.5,与金色面的暖偏同级),
349
+ 不用宿主 green-900 全饱和值——整卡大面积铺色时过绿过亮抢视线(东家视觉反馈)。 */
350
+ background: rgba(32, 41, 35, 0.97);
351
+ }
352
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="error"] {
353
+ --dap-wait-color: #f06a72;
354
+ border-color: color-mix(in srgb, #f06a72 55%, transparent);
355
+ background: rgba(40, 29, 31, 0.97);
356
+ }
357
+ /* 等待卡同为当前会话时描边/光晕回归蓝色高亮(R-01-006/AC-01):基态 [data-current]
358
+ 与 [data-kind="awaiting"] 同优先级且定义在前,深色下被橙色描边顶掉;组合选择器
359
+ (0-4-0)压过两者,深浅主题同值。等待状态仍由圆点、等待徽标与底色承载。 */
360
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-current] {
361
+ border-color: color-mix(in srgb, #65a0ff 75%, transparent);
362
+ box-shadow: 0 0 0 1px color-mix(in srgb, #65a0ff 45%, transparent), 0 0 12px color-mix(in srgb, #65a0ff 30%, transparent);
363
+ }
364
+ /* 所有卡片统一提供可见的悬停反馈;不覆盖当前/等待态自身的颜色语义。 */
365
+ [data-dsh-activity-pane] .dap-card:hover {
366
+ filter: brightness(1.12);
367
+ }
368
+ [data-dsh-activity-pane] .dap-card[data-opening] {
369
+ opacity: 0.85;
370
+ animation: dap-opening 0.9s ease-in-out infinite;
371
+ }
372
+ @keyframes dap-opening { 0%,100% { opacity: 0.95; } 50% { opacity: 0.5; } }
373
+ [data-dsh-activity-pane] .dap-row {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 7px;
377
+ min-width: 0;
378
+ }
379
+ [data-dsh-activity-pane] .dap-dot {
380
+ width: 7px; height: 7px; flex: none; border-radius: 50%;
381
+ background: #58c98f;
382
+ box-shadow: 0 0 7px rgba(88,201,143,.8);
383
+ animation: dap-pulse 1.2s ease-in-out infinite;
384
+ }
385
+ @keyframes dap-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
386
+ /* 等待卡标题状态点静止(R-01-002/AC-08,C-040):闪烁提醒已统一移至卡片末行,
387
+ 标题区不再抢眼;状态点只按等待类别着色——阻塞金、完成绿、错误红。 */
388
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"] .dap-dot {
389
+ animation: none;
390
+ background: var(--dap-wait-color, #58c98f);
391
+ box-shadow: 0 0 8px color-mix(in srgb, var(--dap-wait-color, #58c98f) 85%, transparent);
392
+ }
393
+ [data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-dot {
394
+ background: #8a94a3;
395
+ box-shadow: none;
396
+ animation: none;
397
+ }
398
+ [data-dsh-activity-pane] .dap-title {
399
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
400
+ white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 700;
401
+ }
402
+ /* 最近历史卡标题降为常规字重:历史区不抢占视觉强调(R-01-013/AC-09)。 */
403
+ [data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-title {
404
+ font-weight: 400;
405
+ }
406
+ /* 等待卡末行首行「类型胶囊」(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043):圆底类型
407
+ 图标 + 类型文字,色相随等待类别(--dap-wait-color)——阻塞金/完成绿/错误红;
408
+ 胶囊为行内元素不自占满宽,随文字内容收缩。 */
409
+ [data-dsh-activity-pane] .dap-capsule {
410
+ flex: none; display: inline-flex; align-items: center; gap: 4px;
411
+ font-size: 10px; line-height: 14px; font-weight: 600;
412
+ color: color-mix(in srgb, var(--dap-wait-color, currentColor) 92%, transparent);
413
+ background: color-mix(in srgb, var(--dap-wait-color, currentColor) 14%, transparent);
414
+ border-radius: 999px; padding: 1px 8px 1px 3px;
415
+ }
416
+ /* 胶囊圆底类型图标:圆形底 + 12px 字形,与卡片其它图标尺度对齐。 */
417
+ [data-dsh-activity-pane] .dap-capsule-icon {
418
+ width: 14px; height: 14px; display: inline-flex; align-items: center; justify-content: center;
419
+ border-radius: 50%;
420
+ background: color-mix(in srgb, var(--dap-wait-color, currentColor) 22%, transparent);
421
+ }
422
+ [data-dsh-activity-pane] .dap-capsule-icon:empty { display: none; }
423
+ [data-dsh-activity-pane] .dap-capsule-icon svg { display: block; width: 12px; height: 12px; }
424
+ /* 等待三类的末行脉冲(R-01-002/AC-07、AC-08,C-043、C-065):闪烁载体为卡片末行——
425
+ 类型胶囊与正文固定以 1.2s 同频同相闪烁;「移入历史」按钮不闪。等待队列变化时渲染层
426
+ 将全部末行元素与三处数量胶囊统一重启对相;标题状态点三类均静止。 */
427
+ [data-dsh-activity-pane] .dap-card[data-kind="awaiting"] .dap-foot :is(.dap-capsule, .dap-note) {
428
+ animation: dap-pulse 1.2s ease-in-out infinite;
429
+ }
430
+ /* 复审修复(C-040、C-043):状态点光晕三类同强度、仅换色相——阻塞金、完成绿与错误红光晕一致。 */
431
+ /* 工作区徽标「图标+文本」双段:文件夹图标与左边栏工作区条目同源(R-01-003/AC-06);
432
+ 名称字号不低于 10.5px(AC-07),行高保持 14px 以维持胶囊与卡片高度。
433
+ 着色(AC-08~AC-11):核心映射提供 OKLCH hue;深色主题文字取高明度中高彩度,
434
+ 浅色主题文字取低明度中高彩度。文字直接使用调色板色,底色与描边在 OKLCH
435
+ 空间按透明度混合;前景与背景始终同色相、明度对比拉开。 */
436
+ [data-dsh-activity-pane] .dap-workspace {
437
+ width: fit-content; max-width: 100%; display: flex; align-items: center; gap: 3px;
438
+ overflow: hidden;
439
+ font-size: 10.5px; line-height: 14px;
440
+ --dap-workspace-color: oklch(0.78 0.16 var(--dap-workspace-hue, 235));
441
+ color: var(--dap-workspace-color);
442
+ background: color-mix(in oklch, var(--dap-workspace-color) 14%, transparent);
443
+ border: 1px solid color-mix(in oklch, var(--dap-workspace-color) 34%, transparent);
444
+ border-radius: 999px; padding: 0 7px;
445
+ }
446
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
447
+ --dap-workspace-color: oklch(0.48 0.15 var(--dap-workspace-hue, 235));
448
+ background: color-mix(in oklch, var(--dap-workspace-color) 10%, transparent);
449
+ border-color: color-mix(in oklch, var(--dap-workspace-color) 28%, transparent);
450
+ }
451
+ [data-dsh-activity-pane] .dap-workspace-icon { flex: none; display: inline-flex; }
452
+ [data-dsh-activity-pane] .dap-workspace-icon svg { display: block; }
453
+ [data-dsh-activity-pane] .dap-workspace-text {
454
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
455
+ }
456
+ [data-dsh-activity-pane] .dap-workspace[hidden] { display: none; }
457
+ [data-dsh-activity-pane] .dap-card-head {
458
+ display: flex; align-items: center; gap: 6px; min-width: 0;
459
+ }
460
+ [data-dsh-activity-pane] .dap-card-head .dap-workspace {
461
+ flex: 0 1 auto; min-width: 0;
462
+ }
463
+ [data-dsh-activity-pane] .dap-model {
464
+ flex: 0 1 auto; max-width: 58%; margin-left: auto;
465
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
466
+ color: color-mix(in srgb, currentColor 72%, transparent);
467
+ font-size: 9.5px; line-height: 14px; text-align: right;
468
+ }
469
+ [data-dsh-activity-pane] .dap-model:empty { display: none; }
470
+ [data-dsh-activity-pane] .dap-note {
471
+ min-width: 0; overflow: hidden;
472
+ font-size: 11px; line-height: 15px;
473
+ color: color-mix(in srgb, currentColor 62%, transparent);
474
+ }
475
+ /* 待回复卡使用原生列表语义(R-01-002/AC-09,C-064):单问 ul bullet,多问 ol 编号。 */
476
+ [data-dsh-activity-pane] .dap-question-list {
477
+ margin: 0; padding-inline-start: 18px;
478
+ }
479
+ [data-dsh-activity-pane] .dap-question-list > li { padding-inline-start: 1px; }
480
+ [data-dsh-activity-pane] .dap-question-ellipsis { list-style: none; }
481
+ /* 等待卡末行两段容器(R-01-002/AC-08、AC-09、AC-13,C-043):首行类型胶囊、其下为
482
+ 正文行(正文文字 + 完成提醒卡的「移入历史」按钮——胶囊与正文同闪,按钮不闪)。 */
483
+ [data-dsh-activity-pane] .dap-foot {
484
+ display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
485
+ min-width: 0; margin: 2px 0 0;
486
+ }
487
+ [data-dsh-activity-pane] .dap-note-row {
488
+ display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
489
+ }
490
+ [data-dsh-activity-pane] .dap-note-row .dap-note { flex: 1 1 auto; }
491
+ [data-dsh-activity-pane] .dap-confirm {
492
+ flex: none; padding: 0 7px; margin: 1px 0;
493
+ font-size: 10.5px; line-height: 16px;
494
+ /* 次要按钮(东家视觉反馈):文字压暗不与末行提示争抢,悬停略提仍可辨。 */
495
+ color: color-mix(in srgb, currentColor 58%, transparent);
496
+ background: color-mix(in srgb, currentColor 9%, transparent);
497
+ border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
498
+ border-radius: 5px;
499
+ cursor: pointer;
500
+ }
501
+ [data-dsh-activity-pane] .dap-confirm:hover {
502
+ color: color-mix(in srgb, currentColor 82%, transparent);
503
+ background: color-mix(in srgb, currentColor 14%, transparent);
504
+ }
505
+ /* 运行卡富化(对齐 answer-pet 卡片;MIT 参考,见 README)。 */
506
+ [data-dsh-activity-pane] .dap-progress {
507
+ display: flex; align-items: center; gap: 7px; min-width: 0;
508
+ }
509
+ [data-dsh-activity-pane] .dap-progress .dap-track { flex: 1 1 auto; min-width: 0; }
510
+ [data-dsh-activity-pane] .dap-pct {
511
+ flex: none; width: 5ch; font-size: 12px; line-height: 15px; font-weight: 700; text-align: right;
512
+ color: #9fe8c4; font-variant-numeric: tabular-nums; transform: translateY(-1px);
513
+ }
514
+ [data-dsh-activity-pane] .dap-token-stats {
515
+ display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
516
+ min-width: 0;
517
+ font-size: 10px; line-height: 14px; color: #8f9aaa; font-variant-numeric: tabular-nums;
518
+ }
519
+ /* 左列超长时省略号截断;时长 flex:none 恒贴最右(R-01-009/AC-05)。 */
520
+ [data-dsh-activity-pane] .dap-token-main {
521
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
522
+ }
523
+ [data-dsh-activity-pane] .dap-token-time { flex: none; }
524
+ [data-dsh-activity-pane] .dap-token-stats[hidden] { display: none; }
525
+ [data-dsh-activity-pane] .dap-history-line {
526
+ display: flex; align-items: center; gap: 4px; height: 15px;
527
+ min-width: 0; overflow: hidden; white-space: nowrap;
528
+ font-size: 10.5px; line-height: 15px; color: color-mix(in srgb, currentColor 68%, transparent);
529
+ }
530
+ [data-dsh-activity-pane] .dap-history-line[data-role="agent"] { color: color-mix(in srgb, currentColor 54%, transparent); }
531
+ [data-dsh-activity-pane] .dap-history-icon {
532
+ flex: none; width: 12px; height: 12px; display: inline-flex;
533
+ align-items: center; justify-content: center;
534
+ }
535
+ [data-dsh-activity-pane] .dap-history-icon svg { display: block; width: 12px; height: 12px; }
536
+ [data-dsh-activity-pane] .dap-history-text {
537
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
538
+ }
539
+ [data-dsh-activity-pane] .dap-history-label { flex: none; }
540
+ [data-dsh-activity-pane] .dap-history-separator {
541
+ width: 2px; height: 2px; flex: none; border-radius: 50%;
542
+ background: #778394;
543
+ }
544
+ /* 动作时间线:纵向竖线串起圆点(对齐 answer-pet 的 .ap-session-trace,并修正几何细节——
545
+ 轨道列从卡片内容左边起步,和标题圆点/状态行/进度条共用左边界;竖线回收为容器级
546
+ 整条绘制,节点项只留圆点。时间线节点与 7px 标题点使用同尺寸承载盒(left:0、圆心
547
+ x=3.5),和 1px 竖线保持同一跨 DPR 光栅相位;承载盒 1px border 完全透明,padding-box
548
+ 背景只显示内部 5px 圆核,drop-shadow 从圆核 alpha 生成半透明光晕,不显露 7px 盒边界。
549
+ 1px 竖线 left:3px(圆心 x=3.5),与圆点严格同圆心。竖线为容器
550
+ ::before 单元素整条绘制(对齐层级连接线 .dap-conn-track 的零拼接原则,T-033):
551
+ 逐项分段曾在接缝处双线叠加、半透明相加成亮带(T-069)。线从容器顶贯穿(穿过首个
552
+ 节点圆点并向上引出,表示更早历史被省略),bottom:7px 使终点没入最末圆点内部不外露;
553
+ 单项(含加载行)不画线;圆点高处盖线、位于内容区内不被子代理卡 overflow 裁切。 */
554
+ [data-dsh-activity-pane] .dap-trace {
555
+ position: relative; /* 容器级整条竖线的定位基准 */
556
+ display: flex; flex-direction: column; gap: 3px;
557
+ margin: 1px 0 2px;
558
+ min-width: 0;
559
+ }
560
+ [data-dsh-activity-pane] .dap-trace:empty { display: none; }
561
+ /* 单条连续竖线(z-index 低于圆点):行恒 14px + 间距 3px,最末圆心距容器底 7.5px,
562
+ bottom:7px 使终点没入最末圆点实心核内不外露;:only-child 时无可连接的下一颗圆点,
563
+ 沿用原末项不画线语义。 */
564
+ [data-dsh-activity-pane] .dap-trace::before,
565
+ [data-dsh-activity-pane] .dap-subtrace::before {
566
+ content: ""; position: absolute; left: 3px; top: 0; bottom: 7px;
567
+ width: 1px; z-index: 0;
568
+ background: rgba(126, 147, 177, .3);
569
+ }
570
+ [data-dsh-activity-pane] .dap-trace:has(> :only-child)::before,
571
+ [data-dsh-activity-pane] .dap-subtrace:has(> :only-child)::before { content: none; }
572
+ /* 用户消息行标识(C-019、C-022):行下 1px 中性灰实线下划线,宽度仅为图标+文字的内容
573
+ * 宽度(fit-content 收缩、超长仍按可用宽截断,不贯穿整行),深浅主题各自适配,
574
+ * 不占用状态色(蓝=运行中、绿=完成、红=错误、橙=中断);浅色主题在覆盖块中改色。
575
+ * 以 bottom 1px 背景渐变画实线而非 border-bottom——border 会把 14px 行高撑成 15px,
576
+ * 破坏圆点/竖线节奏;背景不占盒高。 */
577
+ [data-dsh-activity-pane] .dap-trace-item[data-icon="user"] .dap-trace-main {
578
+ width: fit-content;
579
+ max-width: 100%;
580
+ background-image: linear-gradient(rgba(139, 152, 165, .55), rgba(139, 152, 165, .55));
581
+ background-size: 100% 1px;
582
+ background-position: bottom;
583
+ background-repeat: no-repeat;
584
+ }
585
+ [data-dsh-activity-pane] .dap-trace-item {
586
+ position: relative; display: grid;
587
+ grid-template-columns: minmax(0, 1fr) auto;
588
+ column-gap: 7px; min-width: 0;
589
+ padding-left: 14px; /* 左侧轨道:7px 承载盒、竖线与 7px 标题点共用圆心 x=3.5 */
590
+ color: #c7ced9; font-size: 10px; line-height: 14px;
591
+ }
592
+ [data-dsh-activity-pane] .dap-trace-item::before {
593
+ content: ""; position: absolute; left: 0; top: 3px;
594
+ width: 7px; height: 7px;
595
+ box-sizing: border-box; border: 1px solid transparent; border-radius: 50%;
596
+ z-index: 1; /* 圆点盖在竖线上:竖线从圆点中穿过被其遮盖 */
597
+ background-color: #778394; background-clip: padding-box;
598
+ filter: drop-shadow(0 0 1px rgba(119,131,148,.32));
599
+ }
600
+ [data-dsh-activity-pane] .dap-trace-item[data-status="running"]::before {
601
+ background-color: #65a0ff;
602
+ filter: drop-shadow(0 0 1px rgba(101,160,255,.32)) drop-shadow(0 0 3px rgba(101,160,255,.65));
603
+ animation: dap-pulse 1.15s ease-in-out infinite;
604
+ }
605
+ [data-dsh-activity-pane] .dap-trace-item[data-status="done"]::before {
606
+ background-color: #58c98f;
607
+ }
608
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"]::before {
609
+ background-color: #f06a72;
610
+ }
611
+ [data-dsh-activity-pane] .dap-trace-item[data-status="stopped"]::before {
612
+ background-color: #f5a524;
613
+ }
614
+ /* 竖线不在节点项内分段自绘(接缝双线叠加成亮带,T-069),统一由上方容器 ::before
615
+ 整条绘制。 */
616
+ [data-dsh-activity-pane] .dap-trace-icon {
617
+ width: 14px; height: 14px; /* 真实 14px 盒:content-box 下 1px padding 会把盒撑成 16px、
618
+ 抬高时间线行并打破圆点/竖线的 14px 行高几何 */
619
+ flex: none; display: inline-flex;
620
+ align-items: center; justify-content: center; color: #a9b8cc; text-align: center;
621
+ }
622
+ [data-dsh-activity-pane] .dap-trace-icon svg {
623
+ display: block; width: 12px; height: 12px;
624
+ }
625
+ [data-dsh-activity-pane] .dap-trace-main {
626
+ display: flex; align-items: center; gap: 5px; min-width: 0;
627
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
628
+ }
629
+ [data-dsh-activity-pane] .dap-trace-label {
630
+ flex: none; font-weight: 400; color: #c7ced9;
631
+ }
632
+ [data-dsh-activity-pane] .dap-trace-summary {
633
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
634
+ color: #8f9aaa;
635
+ }
636
+ /* 镜像原生 ReasoningRow:钉行尾跟随时不渲染省略号(溢出在左侧行首)。 */
637
+ [data-dsh-activity-pane] .dap-trace-summary[data-follow="end"] { text-overflow: clip; }
638
+ [data-dsh-activity-pane] .dap-trace-detail {
639
+ color: #8f9aaa;
640
+ }
641
+ [data-dsh-activity-pane] .dap-trace-separator {
642
+ width: 2px; height: 2px; flex: none; border-radius: 50%;
643
+ background: #778394;
644
+ }
645
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"] .dap-trace-icon,
646
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"] .dap-trace-label,
647
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"] .dap-trace-summary,
648
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"] .dap-trace-separator {
649
+ color: #f06a72;
650
+ }
651
+ [data-dsh-activity-pane] .dap-trace-item[data-status="error"] .dap-trace-separator {
652
+ background: currentColor;
653
+ }
654
+ /* 子代理:同一节点项几何(轨道/圆点在项内自绘,竖线由容器 ::before 整条绘制);
655
+ 去掉容器级 overflow/padding/border,避免把左侧圆点裁掉。文本截断由 .dap-trace-main
656
+ 自处理。 */
657
+ [data-dsh-activity-pane] .dap-subtrace {
658
+ position: relative; /* 容器级整条竖线的定位基准 */
659
+ min-width: 0;
660
+ font-size: 10px; line-height: 14px;
661
+ color: color-mix(in srgb, currentColor 72%, transparent);
662
+ margin: 1px 0 0;
663
+ }
664
+ [data-dsh-activity-pane] .dap-subtrace .dap-trace-item {
665
+ color: inherit;
666
+ }
667
+ [data-dsh-activity-pane] .dap-track {
668
+ position: relative; height: 5px; border-radius: 6px; overflow: hidden;
669
+ background: rgba(255, 255, 255, 0.11);
670
+ }
671
+ [data-dsh-activity-pane] .dap-fill {
672
+ position: absolute; inset: 0 auto 0 0; width: 0%;
673
+ border-radius: 6px;
674
+ background: repeating-linear-gradient(90deg, #58c98f 0 10px, #3fbf86 10px 20px);
675
+ background-size: 200% 100%;
676
+ box-shadow: 0 0 7px rgba(88, 201, 143, 0.5);
677
+ transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
678
+ /* 进度条仅存于运行卡骨架:会话运行全程持续向右滚动条纹,作为活动标志
679
+ (对齐 answer-pet 的 ap-stripes,R-01-009/AC-08)。 */
680
+ animation: dap-stripes 0.8s linear infinite;
681
+ }
682
+ @keyframes dap-stripes {
683
+ from { background-position: 0 0; }
684
+ to { background-position: 40px 0; }
685
+ }
686
+ @media (prefers-reduced-motion: reduce) {
687
+ /* answer-pet 保留状态脉冲/进度条纹;仅关闭宽度过渡,避免状态反馈消失。 */
688
+ [data-dsh-activity-pane] .dap-fill { transition: none; }
689
+ }
690
+ [data-dsh-activity-pane] .dap-empty {
691
+ padding: 14px 12px; font-size: 12px; text-align: center;
692
+ color: color-mix(in srgb, currentColor 45%, transparent);
693
+ }
694
+ [data-dsh-activity-pane] .dap-empty[data-mode="loading"] {
695
+ display: flex; align-items: center; justify-content: center; gap: 6px;
696
+ }
697
+ /* 加载指示:列表/卡片字段在途时的活动图标(R-01-014)。 */
698
+ [data-dsh-activity-pane] .dap-spinner {
699
+ width: 10px; height: 10px; flex: none; border-radius: 50%;
700
+ border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
701
+ border-top-color: color-mix(in srgb, currentColor 85%, transparent);
702
+ animation: dap-spin 0.8s linear infinite;
703
+ box-sizing: border-box;
704
+ display: inline-block;
705
+ }
706
+ @keyframes dap-spin { to { transform: rotate(360deg); } }
707
+ [data-dsh-activity-pane] .dap-model .dap-spinner,
708
+ [data-dsh-activity-pane] .dap-history-line .dap-spinner {
709
+ width: 8px; height: 8px; border-width: 1.5px;
710
+ }
711
+ /* 数量标识加载指示:列表在途时三处徽标内显示活动图标(R-01-014/AC-06)。 */
712
+ [data-dsh-activity-pane] .dap-count .dap-spinner,
713
+ [data-dsh-activity-pane] .dap-rail-count .dap-spinner,
714
+ .dap-toggle .dap-toggle-count .dap-spinner {
715
+ vertical-align: middle;
716
+ }
717
+ /* 移动端浮动开关按钮:仅在窄屏显示(桌面隐藏)。固定于会话头部左上角、
718
+ 原生左边栏切换按钮(28px @ left:8px; top:12px)右侧(R-01-008/AC-04)。 */
719
+ .dap-toggle {
720
+ position: fixed; top: 12px; left: 44px; z-index: 2147482991;
721
+ display: none;
722
+ align-items: center; gap: 6px;
723
+ min-height: 30px; padding: 0 11px;
724
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
725
+ border-radius: 999px;
726
+ background: rgba(24, 28, 38, 0.94);
727
+ color: currentColor;
728
+ font-size: 12px; font-weight: 600;
729
+ cursor: pointer;
730
+ box-shadow: 0 6px 16px rgba(0,0,0,.34);
731
+ }
732
+ .dap-toggle .dap-toggle-count {
733
+ min-width: 16px; text-align: center; border-radius: 999px;
734
+ background: color-mix(in srgb, currentColor 16%, transparent);
735
+ padding: 0 5px; font-size: 10px; font-weight: 700;
736
+ }
737
+ .dap-toggle[data-awaiting] .dap-toggle-count {
738
+ background: rgba(46, 42, 26, 0.97);
739
+ animation: dap-await-pulse 1.2s ease-in-out infinite;
740
+ }
741
+ /* 移动端抽屉透明遮罩:抽屉打开时铺满视口、点击收起抽屉(R-01-008/AC-03)。
742
+ 完全透明不占布局;z-index 介于主会话与抽屉(2147482990)之间;抽屉打开期间
743
+ 浮动开关隐藏(见 .dap-toggle[data-drawer-open],R-01-008/AC-05),关闭由抽屉
744
+ 头部 × 与遮罩承担;桌面断点外由媒体查询保持隐藏。 */
745
+ .dap-backdrop {
746
+ position: fixed; inset: 0;
747
+ z-index: 2147482989;
748
+ background: transparent;
749
+ display: none;
750
+ }
751
+ /* 活动区↔历史区迁移动画(R-01-010/AC-07、AC-10):旧卡克隆 ghost 挂载于窗格元素内
752
+ (卡片样式经 [data-dsh-activity-pane] 作用域自然生效),以 absolute 覆盖层从原矩形
753
+ FLIP 平移并形变至目标区卡片矩形,到位后再淡出、真卡同步淡入;ghost 生命周期由
754
+ transitionend 收口。迁移帧内位置受影响的其它卡片(含历史区段头)经 dap-shift
755
+ 反向位移平滑归位,不瞬间跳变。prefers-reduced-motion 时 JS 侧整体跳过(不创建
756
+ ghost、不加 dap-move-in、不量取受影响卡片矩形)。 */
757
+ [data-dsh-activity-pane] > .dap-move-ghost {
758
+ position: absolute;
759
+ z-index: 6;
760
+ margin: 0;
761
+ pointer-events: none;
762
+ transition: transform 0.3s ease, width 0.3s ease, height 0.3s ease, opacity 0.1s ease 0.2s;
763
+ }
764
+ [data-dsh-activity-pane] .dap-move-in {
765
+ animation: dap-move-in 0.3s ease;
766
+ }
767
+ @keyframes dap-move-in { from { opacity: 0; } }
768
+ /* 受影响卡片 FLIP(R-01-010/AC-10):JS 先以无过渡内联 transform 把元素钉回旧视觉
769
+ 位置,reflow 后挂本类并清除内联位移,经 transform 过渡平滑归位;与 ghost 同时长
770
+ 同缓动。.dap-card/.dap-recent-head 均无既有 transition 规则,本类不覆盖其它过渡。 */
771
+ [data-dsh-activity-pane] .dap-shift {
772
+ transition: transform 0.3s ease;
773
+ }
774
+ /* 窄屏:窗格变为固定抽屉 + 浮动开关按钮;抽屉默认隐藏在屏外。
775
+ 抽屉需不透明背景(否则透出下层会话内容)+ touch-action:none(把手/头部的
776
+ 触摸不滚动下层页面),桌面列则保持低透明分界。 */
777
+ @media (max-width: ${MOBILE_BREAKPOINT}) {
778
+ [data-dsh-activity-pane] {
779
+ position: fixed; left: 0; top: 0; bottom: 0;
780
+ width: min(84vw, 320px);
781
+ margin: 0;
782
+ border-right: 1px solid currentColor;
783
+ background: var(--dsw-alias-bg-layer-2, rgba(18, 21, 27, 0.97));
784
+ backdrop-filter: blur(10px);
785
+ -webkit-backdrop-filter: blur(10px);
786
+ box-shadow: 8px 0 28px rgba(0,0,0,.5);
787
+ transform: translateX(-102%);
788
+ transition: transform 180ms ease;
789
+ z-index: 2147482990;
790
+ touch-action: none;
791
+ }
792
+ [data-dsh-activity-pane][data-open="true"] { transform: translateX(0); }
793
+ .dap-backdrop[data-drawer-open] { display: block; }
794
+ .dap-toggle { display: flex; }
795
+ .dap-toggle[data-drawer-open] { display: none; }
796
+ }
797
+ /* 浅色主题适配:外壳以 body 上 data-ds-dark-theme 属性标记深色(缺省即浅色),
798
+ 并在两个作用域下翻转整套 --dsw-alias-* 变量。本块只覆盖上文暗色专用的硬编码
799
+ 颜色(卡片底色/描边/阴影、时间线文字与轨道、进度轨道、浮动开关),深色主题下
800
+ 全部规则保持原值;状态色(绿/蓝/橙/红)与 currentColor 派生色两主题通用,不覆盖。
801
+ ::before 状态圆点不覆盖:基色本就被各 data-status 规则接管,覆盖反而会以更高
802
+ 优先级压掉运行/完成/错误状态色。 */
803
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card {
804
+ background: var(--dsw-alias-bg-layer-2, #ffffff);
805
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
806
+ box-shadow: var(--dsw-shadow-lv2, 0 6px 16px rgba(0, 0, 0, 0.12));
807
+ }
808
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card:hover {
809
+ border-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, 0.16));
810
+ filter: brightness(0.97);
811
+ }
812
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="subagent"] {
813
+ background: var(--dsw-specific-sidebar-fill, rgb(249, 250, 251));
814
+ }
815
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="recent"] {
816
+ /* 暗于活动卡的 --dsw-alias-bg-layer-2 纯白、深于窗格底色(R-01-013/AC-10、AC-11)。 */
817
+ background: rgb(243, 244, 246);
818
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
819
+ }
820
+ /* 当前会话高亮(R-01-006/AC-01):蓝色描边/光晕两主题同值,但浅色块必须在
821
+ [data-kind] 覆盖之后重声明——浅色 .dap-card/:hover/[data-kind] 规则的优先级均高于
822
+ 基态 [data-current] 规则,不重声明则浅色下选中描边与光晕被顶掉。 */
823
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-current] {
824
+ border-color: color-mix(in srgb, #65a0ff 75%, transparent);
825
+ box-shadow: 0 0 0 1px color-mix(in srgb, #65a0ff 45%, transparent), 0 0 12px color-mix(in srgb, #65a0ff 30%, transparent);
826
+ }
827
+
828
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="blocked"] {
829
+ background: rgb(253, 244, 208);
830
+ }
831
+ /* 完成提醒卡浅色主题取宿主 success 三级背景别名(R-01-002/AC-04,C-040):淡绿成功底。 */
832
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="done"] {
833
+ background: var(--dsw-alias-state-success-tertiary, rgb(230, 250, 237));
834
+ }
835
+ /* 错误提醒卡浅色主题:与深色错误红同源的淡红错误底(C-043)。 */
836
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="awaiting"][data-wait="error"] {
837
+ background: rgb(252, 233, 234);
838
+ }
839
+ /* 数量徽标等待态浅色主题覆盖:默认金色跟随阻塞卡、tone=done 跟随完成卡别名、
840
+ tone=error 跟随错误卡淡红(R-01-002/AC-06,无描边与外环)。 */
841
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-count[data-awaiting],
842
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-rail-count[data-awaiting],
843
+ body:not([data-ds-dark-theme]) .dap-toggle[data-awaiting] .dap-toggle-count {
844
+ background: rgb(253, 244, 208);
845
+ }
846
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-count[data-awaiting][data-tone="done"],
847
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-rail-count[data-awaiting][data-tone="done"],
848
+ body:not([data-ds-dark-theme]) .dap-toggle[data-awaiting][data-tone="done"] .dap-toggle-count {
849
+ background: var(--dsw-alias-state-success-tertiary, rgb(230, 250, 237));
850
+ }
851
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-count[data-awaiting][data-tone="error"],
852
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-rail-count[data-awaiting][data-tone="error"],
853
+ body:not([data-ds-dark-theme]) .dap-toggle[data-awaiting][data-tone="error"] .dap-toggle-count {
854
+ background: rgb(252, 233, 234);
855
+ }
856
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-pct {
857
+ color: var(--dsw-alias-state-success-primary, #22c55e);
858
+ }
859
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-token-stats,
860
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-summary,
861
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-detail,
862
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-icon {
863
+ color: var(--dsw-alias-label-tertiary, rgb(129, 133, 140));
864
+ }
865
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-item,
866
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-label {
867
+ color: var(--dsw-alias-label-secondary, rgb(97, 102, 107));
868
+ }
869
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-separator,
870
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-history-separator {
871
+ background: var(--dsw-alias-label-caption, rgb(173, 178, 184));
872
+ }
873
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace::before,
874
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-subtrace::before {
875
+ background: var(--dsw-alias-border-l3, rgba(0, 0, 0, 0.12));
876
+ }
877
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-item[data-icon="user"] .dap-trace-main {
878
+ background-image: linear-gradient(var(--dsw-alias-label-tertiary, rgb(129, 133, 140)), var(--dsw-alias-label-tertiary, rgb(129, 133, 140)));
879
+ }
880
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
881
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08));
882
+ }
883
+ body:not([data-ds-dark-theme]) .dap-toggle {
884
+ background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
885
+ }
886
+ /* 「回到顶部」图标按钮浅色覆盖:不透明层-2 底色与外壳描边别名(R-01-018/AC-05)。 */
887
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top {
888
+ background: var(--dsw-alias-bg-layer-2, #ffffff);
889
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
890
+ }
891
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:hover,
892
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:focus-visible {
893
+ background: var(--dsw-alias-bg-layer-3, #eceef1);
894
+ }
895
+ `;
896
+
897
+ function getSnapshot(service, key) {
898
+ try {
899
+ return service?.[key]?.getSnapshot?.() ?? null;
900
+ } catch {
901
+ return null;
902
+ }
903
+ }
904
+
905
+ function schedule(callback) {
906
+ if (typeof window.requestAnimationFrame === "function") {
907
+ window.requestAnimationFrame(callback);
908
+ } else {
909
+ queueMicrotask(callback);
910
+ }
911
+ }
912
+
913
+ /** Map 键数组兜底:非 Map 输入归一空数组(快照字段防御性读取)。 */
914
+ function mapKeys(value) {
915
+ return value instanceof Map ? [...value.keys()] : [];
916
+ }
917
+
918
+ /** 从原生会话快照归一运行卡回合开始时间。
919
+ * elapsed 不在快照事件时固化——渲染期用 Date.now()-startTime 实时算,时长才能
920
+ * 随 1s 时钟逐秒跳动(R-01-009/AC-03)。 */
921
+ function livenessFromSnapshot(snap) {
922
+ let startTime = null;
923
+ const timings = snap?.turnTimings;
924
+ if (timings instanceof Map) {
925
+ for (const timing of timings.values()) {
926
+ if (
927
+ timing &&
928
+ Number.isFinite(timing.startTime) &&
929
+ timing.endTime === undefined
930
+ ) {
931
+ startTime = timing.startTime;
932
+ break;
933
+ }
934
+ }
935
+ }
936
+ return { startTime };
937
+ }
938
+
939
+ function fmtRecentTime(ts) {
940
+ try {
941
+ return `最近 ${new Date(ts).toLocaleTimeString([], {
942
+ hour: "2-digit",
943
+ minute: "2-digit",
944
+ })}`;
945
+ } catch {
946
+ return "";
947
+ }
948
+ }
949
+
950
+ /** 读取持久化列宽:缺失/非法/越界值经 clampPaneWidth 归一;localStorage 不可用(隐私模式)静默回退默认(R-01-015/AC-04)。 */
951
+ function readStoredPaneWidth() {
952
+ try {
953
+ return clampPaneWidth(window.localStorage.getItem(WIDTH_STORAGE_KEY));
954
+ } catch {
955
+ return PANE_WIDTH_DEFAULT;
956
+ }
957
+ }
958
+ /** 拖拽结束持久化列宽;localStorage 不可用时静默跳过(R-01-015/AC-04)。 */
959
+ function writeStoredPaneWidth(width) {
960
+ try {
961
+ window.localStorage.setItem(WIDTH_STORAGE_KEY, String(width));
962
+ } catch {}
963
+ }
964
+
965
+ function apply(ctx) {
966
+ const previousCleanup = document[INSTANCE_KEY] ?? globalThis[INSTANCE_KEY];
967
+ if (typeof previousCleanup === "function") previousCleanup();
968
+ for (const node of document.querySelectorAll(
969
+ `[${PANE_ATTR}], .dap-toggle, .dap-backdrop, .dap-move-ghost, #${STYLE_ID}`,
970
+ ))
971
+ node.remove();
972
+ let disposed = false;
973
+ let sessions = null;
974
+ let workspaces = null;
975
+ let sessionUnsubscribe = null;
976
+ let workspaceUnsubscribe = null;
977
+ let clockTimer = null;
978
+ let syncScheduled = false;
979
+ let lastSig = "";
980
+ /** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
981
+ let pulseSignature = "";
982
+ // E2E-only deterministic seams(T-089/T-090):显式 URL fragment 最多把本页首次列表 ready 呈现
983
+ // 或正式 models RPC 延后 1s;默认路径为 0,不伪造服务响应、快照或生产时序。
984
+ const e2eParams = new URLSearchParams(window.location.hash.slice(1));
985
+ const requestedListDelay = Number(e2eParams.get("dap-e2e-list-delay"));
986
+ const e2eListReadyAt = Number.isFinite(requestedListDelay) && requestedListDelay > 0
987
+ ? Date.now() + Math.min(requestedListDelay, 1_000)
988
+ : 0;
989
+ const requestedModelDelay = Number(e2eParams.get("dap-e2e-model-delay"));
990
+ const e2eModelDelayMs = Number.isFinite(requestedModelDelay) && requestedModelDelay > 0
991
+ ? Math.min(requestedModelDelay, 1_000)
992
+ : 0;
993
+ let e2eListReleaseTimer = null;
994
+ const e2eModelDelayWaiters = new Map();
995
+ function delayedModelCall(call) {
996
+ if (e2eModelDelayMs === 0) return Promise.resolve().then(call);
997
+ return new Promise((resolve) => {
998
+ const timer = setTimeout(() => {
999
+ e2eModelDelayWaiters.delete(timer);
1000
+ resolve(disposed ? null : Promise.resolve().then(call));
1001
+ }, e2eModelDelayMs);
1002
+ e2eModelDelayWaiters.set(timer, resolve);
1003
+ });
1004
+ }
1005
+ let renderedPane = null;
1006
+ let boundPane = null;
1007
+ let unbindPaneControls = null;
1008
+ let collapsed = false;
1009
+ /** 当前桌面列宽:启动时从 localStorage 恢复,拖拽实时更新,重挂载后保留(R-01-015)。 */
1010
+ let paneWidth = readStoredPaneWidth();
1011
+ /** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
1012
+ let lastActivatedId = null;
1013
+ /** 完成确认状态(R-01-002/AC-03、AC-10~AC-12):id → { lastTurnEnd, ackedAt },
1014
+ * 来自宿主侧 ack 通道(SSE 全量快照),随每次推送整体替换。 */
1015
+ const completeAcksById = new Map();
1016
+ /** 上一帧已提交渲染的活动区 id 集合:活动区→历史区迁移检测(R-01-010/AC-07)。 */
1017
+ let prevRenderedActiveIds = new Set();
1018
+ /** 上一帧已提交渲染的历史区 id 集合:历史区→活动区迁移检测(R-01-010/AC-07)。 */
1019
+ let prevRenderedRecentIds = new Set();
1020
+ /** 迁移中 ghost 元素集合(含 id 索引):同一 id 再迁移时旧 ghost 移除,卸载时统一清理。 */
1021
+ const moveGhosts = new Set();
1022
+ const moveGhostsById = new Map();
1023
+ /** 在飞平移的元素级清理(元素 → transitionend 监听移除 + 类与内联位移复位):
1024
+ * 平移中再次迁移以当前视觉矩形为起点重启;prune 与卸载时同步取消(R-01-010/AC-10)。 */
1025
+ const shiftCleanups = new Map();
1026
+ /** 活动区卡片 id → { el, kind } 复用表。 */
1027
+ const cardsById = new Map();
1028
+ /** 历史区卡片 id → { el } 复用表。 */
1029
+ const recentCardsById = new Map();
1030
+ /** 运行中会话原生快照订阅:id → { unsubscribe, liveness, snapshot }。 */
1031
+ const livenessById = new Map();
1032
+ /** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
1033
+ const progressAnchorById = new Map();
1034
+ /** 订阅停止后保留最近快照,供 awaiting/recent 卡继续显示上下文。 */
1035
+ const sessionDetailsById = new Map();
1036
+ /** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
1037
+ const openRetryStates = new Map();
1038
+ /** native cold-session model/history reads, one promise per session and no polling. */
1039
+ const modelLoads = new Map();
1040
+ const historyLoads = new Map();
1041
+ /** 模型目录订阅(R-01-012/AC-16):id → unsubscribe;模型选择切换经原生
1042
+ * modelDirectories store 推送即时到达,随可见性清理/卸载先 unsubscribe 再除名。 */
1043
+ const modelDirectorySubs = new Map();
1044
+ /** native session.open() requests in flight; avoid duplicate cold history reads. */
1045
+ /** 冷数据读取并发池:队列顺序即优先级(调用方已排序),逐个完成逐个重绘。 */
1046
+ const loadQueue = [];
1047
+ let loadInflight = 0;
1048
+ function pumpDetailLoads() {
1049
+ while (loadInflight < LOAD_CONCURRENCY && loadQueue.length > 0) {
1050
+ const job = loadQueue.shift();
1051
+ loadInflight += 1;
1052
+ job().finally(() => {
1053
+ loadInflight = Math.max(0, loadInflight - 1);
1054
+ if (!disposed) pumpDetailLoads();
1055
+ });
1056
+ }
1057
+ }
1058
+ function enqueueDetailLoad(job) {
1059
+ const promise = new Promise((resolve) => {
1060
+ loadQueue.push(() => job().then(resolve, resolve));
1061
+ });
1062
+ pumpDetailLoads();
1063
+ return promise;
1064
+ }
1065
+ const sessionOpenLoads = new Map();
1066
+
1067
+ const style = document.createElement("style");
1068
+ style.id = STYLE_ID;
1069
+ style.textContent = CSS;
1070
+ document.head.appendChild(style);
1071
+
1072
+ // 移动端浮动开关按钮(桌面隐藏见 CSS)。
1073
+ const toggle = document.createElement("button");
1074
+ toggle.className = "dap-toggle";
1075
+ toggle.type = "button";
1076
+ toggle.setAttribute("aria-label", "切换活动会话窗格");
1077
+ toggle.innerHTML =
1078
+ "<span>活动</span><span class=\"dap-toggle-count\"></span>";
1079
+ document.body.appendChild(toggle);
1080
+
1081
+ // 移动端抽屉透明遮罩:仅窄屏且抽屉打开时显示,点击收起抽屉(R-01-008/AC-03)。
1082
+ const backdrop = document.createElement("div");
1083
+ backdrop.className = "dap-backdrop";
1084
+ document.body.appendChild(backdrop);
1085
+
1086
+ // 桌面判定与"真实参与布局":中间列切为行方向,窗格固定宽、会话根弹性填充
1087
+ // 剩余宽度(flex:1 + min-width:0),让会话内容(标题/tabs/滚动区/输入框)
1088
+ // 被真实挤到窗格右侧,且聊天列在增宽的会话带内自动居中。
1089
+ const desktopQuery = window.matchMedia("(min-width: 768px)");
1090
+ /** 找到作为 center 弹性子项的真实会话盒(跳过 display:contents 占位层)。 */
1091
+ function conversationFlexItem(center) {
1092
+ const scroll = document.querySelector("[data-conversation-scroll]");
1093
+ if (scroll === null) return null;
1094
+ let node = scroll;
1095
+ let candidate = null;
1096
+ while (node !== null && node !== center && node !== document.body) {
1097
+ let display = "";
1098
+ try {
1099
+ display = window.getComputedStyle(node).display;
1100
+ } catch {}
1101
+ if (display !== "contents") candidate = node;
1102
+ node = node.parentElement;
1103
+ }
1104
+ return candidate;
1105
+ }
1106
+ function applyLayout() {
1107
+ const seat = document.querySelector(CONVERSATION_SELECTOR);
1108
+ if (seat === null || seat.parentElement === null) return;
1109
+ const center = seat.parentElement;
1110
+ let changed = false;
1111
+ if (!desktopQuery.matches) {
1112
+ // 移动端:抽屉脱离文档流(fixed),不参与主会话布局;恢复外壳默认
1113
+ // 列布局,避免行方向扰动移动端主会话显示(R-01-008)。
1114
+ if (center.style.flexDirection !== "") {
1115
+ center.style.flexDirection = "";
1116
+ changed = true;
1117
+ }
1118
+ if (center.style.alignItems !== "") {
1119
+ center.style.alignItems = "";
1120
+ changed = true;
1121
+ }
1122
+ const flex = conversationFlexItem(center);
1123
+ if (flex !== null) {
1124
+ if (flex.style.flex !== "") {
1125
+ flex.style.flex = "";
1126
+ changed = true;
1127
+ }
1128
+ if (flex.style.minWidth !== "") {
1129
+ flex.style.minWidth = "";
1130
+ changed = true;
1131
+ }
1132
+ }
1133
+ if (changed) notifyLayoutChange();
1134
+ return;
1135
+ }
1136
+ if (center.style.flexDirection !== "row") {
1137
+ center.style.flexDirection = "row";
1138
+ changed = true;
1139
+ }
1140
+ if (center.style.alignItems !== "stretch") {
1141
+ center.style.alignItems = "stretch";
1142
+ changed = true;
1143
+ }
1144
+ const flex = conversationFlexItem(center);
1145
+ if (flex !== null) {
1146
+ if (flex.style.flex !== "1 1 0%") {
1147
+ flex.style.flex = "1 1 0%";
1148
+ changed = true;
1149
+ }
1150
+ if (flex.style.minWidth !== "0") {
1151
+ flex.style.minWidth = "0";
1152
+ changed = true;
1153
+ }
1154
+ }
1155
+ if (changed) notifyLayoutChange();
1156
+ }
1157
+
1158
+ function queueSync() {
1159
+ if (disposed || syncScheduled) return;
1160
+ syncScheduled = true;
1161
+ schedule(() => {
1162
+ syncScheduled = false;
1163
+ if (!disposed) render();
1164
+ });
1165
+ }
1166
+
1167
+ // ---- 完成确认通道(R-01-002/AC-10~AC-12、R-01-010/AC-06,C-030) ----
1168
+ // SSE 订阅宿主侧 ack 状态:连接即收全量快照(刷新/重连恢复),此后每次变更
1169
+ // (任一客户端的确认、任一回合结束)推送新全量。无 EventSource 环境静默降级
1170
+ // 为无完成提醒(宿主侧不可用时插件其余功能不受影响)。
1171
+ // 移动 PWA 退后台时系统掐断连接,恢复时 EventSource 常以 CLOSED 终态落地不再
1172
+ // 自动重连(或半开死连接看似 OPEN 却不再收到广播),本地 ack 状态随之永久停滞;
1173
+ // 故回到前台时无条件重建连接——连接即收全量快照,各失效形态一并收敛
1174
+ // (R-01-002/AC-12 缺陷回归:完成等待中的会话曾被误判入历史区直至整页重载)。
1175
+ let acksSource = null;
1176
+
1177
+ /** SSE 全量快照应用:解析成功才整体替换本地状态并重绘;坏帧静默丢弃。 */
1178
+ function applyAcksState(raw) {
1179
+ if (disposed) return;
1180
+ let state = null;
1181
+ try {
1182
+ state = JSON.parse(raw);
1183
+ } catch {
1184
+ state = null;
1185
+ }
1186
+ if (state === null || typeof state !== "object") return;
1187
+ completeAcksById.clear();
1188
+ for (const [id, record] of Object.entries(state)) {
1189
+ const lastTurnEnd = Number(record?.lastTurnEnd);
1190
+ if (Number.isFinite(lastTurnEnd) && lastTurnEnd > 0) {
1191
+ completeAcksById.set(String(id), {
1192
+ lastTurnEnd,
1193
+ lastTurnEndKind: typeof record?.lastTurnEndKind === "string" ? record.lastTurnEndKind : null,
1194
+ lastTurnEndError: typeof record?.lastTurnEndError === "string" ? record.lastTurnEndError : null,
1195
+ ackedAt: Number(record.ackedAt) || null,
1196
+ });
1197
+ }
1198
+ }
1199
+ queueSync();
1200
+ }
1201
+
1202
+ /** (重)建 SSE 连接:先关闭旧连接再新建;连接即收宿主侧全量快照。 */
1203
+ function connectAcksStream() {
1204
+ try {
1205
+ acksSource?.close();
1206
+ } catch {}
1207
+ acksSource = null;
1208
+ if (disposed || typeof window.EventSource !== "function") return;
1209
+ try {
1210
+ const source = new window.EventSource(`${ACK_API_BASE}/acks/stream`);
1211
+ source.addEventListener("state", (event) => applyAcksState(event.data ?? ""));
1212
+ acksSource = source;
1213
+ } catch {
1214
+ acksSource = null;
1215
+ }
1216
+ }
1217
+
1218
+ /** 回到前台(含 bfcache 还原)时 ack 通道自愈:重建 SSE,借连接全量快照收敛。 */
1219
+ function resumeAcksChannel() {
1220
+ if (!disposed) connectAcksStream();
1221
+ }
1222
+ const onVisibilityResume = () => {
1223
+ if (document.visibilityState === "visible") resumeAcksChannel();
1224
+ };
1225
+ const onPageShow = (event) => {
1226
+ if (event?.persisted === true) resumeAcksChannel();
1227
+ };
1228
+ document.addEventListener("visibilitychange", onVisibilityResume);
1229
+ window.addEventListener("pageshow", onPageShow);
1230
+ connectAcksStream();
1231
+
1232
+ /** 确认写回(R-01-002/AC-10~AC-12):乐观更新本地游标(签名驱动即时解除),
1233
+ * 再 POST 宿主侧持久化并广播;写回失败回滚本地游标(提醒恢复),不吞异常。 */
1234
+ async function ackCompletion(sessionId) {
1235
+ const id = String(sessionId);
1236
+ const prev = completeAcksById.get(id) ?? null;
1237
+ completeAcksById.set(id, { ...prev, lastTurnEnd: prev?.lastTurnEnd ?? null, ackedAt: Date.now() });
1238
+ queueSync();
1239
+ try {
1240
+ const response = await fetch(`${ACK_API_BASE}/ack`, {
1241
+ method: "POST",
1242
+ headers: { "Content-Type": "application/json" },
1243
+ body: JSON.stringify({ sessionId: id }),
1244
+ });
1245
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1246
+ } catch (error) {
1247
+ console.warn(`[dsh-activity-pane] 完成确认写回失败(${id}):`, error);
1248
+ if (prev !== null) completeAcksById.set(id, prev);
1249
+ else completeAcksById.delete(id);
1250
+ queueSync();
1251
+ }
1252
+ }
1253
+
1254
+ function apiValue(response) {
1255
+ return response?.result?.ok === true ? response.result.value : null;
1256
+ }
1257
+
1258
+ /** 同帧重启等待提醒动画:数量胶囊保持亮度呼吸,卡片末行保持 opacity 脉冲,
1259
+ * 只统一周期起点,不改变各自视觉载体(R-01-002/AC-07、AC-08,C-065)。 */
1260
+ function syncAwaitPulse(nodes) {
1261
+ const activeNodes = [...new Set(nodes)].filter((node) => node?.isConnected);
1262
+ if (activeNodes.length === 0) return;
1263
+ for (const node of activeNodes) node.style.animation = "none";
1264
+ void activeNodes[0].offsetWidth;
1265
+ for (const node of activeNodes) node.style.animation = "";
1266
+ }
1267
+
1268
+ /** 模型目录订阅(R-01-012/AC-16,C-024):订阅原生 modelDirectories 服务的
1269
+ * per-session 目录 store(与主会话窗口模型选择器同源),同客户端切换模型选择
1270
+ * 经 select() 成功即推送,到达即重归一模型上下文并就地重绘。
1271
+ * 只订阅不 load()——load 会推进目录 generation 计数,与进行中的 select() 竞争
1272
+ * 会致其更新被「最新操作胜出」规则丢弃;初值与失败语义沿用一次性 RPC 路径。
1273
+ * 服务缺失、会话无 scope 或订阅失败时静默回落为纯一次性读取。 */
1274
+ function subscribeModelDirectory(id, detail) {
1275
+ if (modelDirectorySubs.has(id)) return;
1276
+ let directory = null;
1277
+ try {
1278
+ directory = ctx.get("modelDirectories")?.directoryFor?.(id) ?? null;
1279
+ } catch {
1280
+ directory = null; // 会话无 scope:回落一次性读取
1281
+ }
1282
+ if (directory === null) return;
1283
+ const syncFromDirectory = () => {
1284
+ if (disposed) return;
1285
+ const snap = directory.store?.getSnapshot?.();
1286
+ if (!snap?.current) return; // 目录未就绪:不覆写既有取值
1287
+ detail.models = { current: snap.current, groups: snap.groups ?? [] };
1288
+ detail.model = modelMetadata(detail.models);
1289
+ // 订阅已产值标记:晚到的一次性 RPC 快照不得回写切换前的旧值。
1290
+ detail.modelLive = true;
1291
+ queueSync();
1292
+ };
1293
+ let unsubscribe = null;
1294
+ try {
1295
+ unsubscribe = directory.store.subscribe(syncFromDirectory);
1296
+ } catch {
1297
+ return; // 订阅失败:保持一次性读取结果
1298
+ }
1299
+ modelDirectorySubs.set(id, unsubscribe);
1300
+ syncFromDirectory(); // 目录已被主窗口加载时立即同步,该会话免发一次性 RPC
1301
+ }
1302
+
1303
+ function loadNativeDetails(ids, previewFallbackIds = new Set()) {
1304
+ const api = ctx.get("connection")?.api?.sessions;
1305
+ if (!api) return;
1306
+ const byId = getSnapshot(sessions, "list")?.byId ?? {};
1307
+ const modelPromises = [];
1308
+ const historyPromises = [];
1309
+ for (const id of ids) {
1310
+ const detail = sessionDetailsById.get(id) ?? {};
1311
+ const liveSnapshot = livenessById.get(id)?.snapshot;
1312
+ if (liveSnapshot) detail.snapshot = liveSnapshot;
1313
+ sessionDetailsById.set(id, detail);
1314
+ const subagent = isSubagentRow(byId[id], byId);
1315
+ // 主会话先试模型目录订阅:store 已有当前选择时同步填充(随后 plan.model 为假、免发 RPC);
1316
+ // 子代理的目录不可用(宿主以 agent-busy 拒绝其模型 RPC),不建立订阅。
1317
+ if (!subagent && e2eModelDelayMs === 0) subscribeModelDirectory(id, detail);
1318
+ const snapshotReady = detail.snapshot?.openState === "open";
1319
+ // 冷窗口兜底(R-01-009/AC-06、R-01-012/AC-12):快照就绪但窗口缺开放回合起点
1320
+ // (超长回合的 turn/start 在尾页窗口之外;等待/空闲会话不算缺口)或缺可锚
1321
+ // 用户行时,补读一次 history。
1322
+ const turnStartMissing = openTurnStartMissing({
1323
+ snapshotReady,
1324
+ running: byId[id]?.running === true,
1325
+ hasLiveness: livenessById.has(id),
1326
+ liveStartTime: livenessById.get(id)?.liveness?.startTime ?? null,
1327
+ });
1328
+ const windowComplete = snapshotReady !== true || (!turnStartMissing && detail.snapshotHasAnchorableUserRow === true);
1329
+ const plan = detailLoadPlan({
1330
+ detail,
1331
+ isSubagent: subagent,
1332
+ snapshotReady,
1333
+ historyNeeded: needsHistorySnapshot(detail.snapshot),
1334
+ previewFallbackNeeded: previewFallbackIds.has(id),
1335
+ windowComplete,
1336
+ modelInflight: modelLoads.has(id),
1337
+ historyInflight: historyLoads.has(id) || sessionOpenLoads.has(id),
1338
+ });
1339
+ if (plan.subagent) {
1340
+ // 子代理的 models 读取必被宿主以 agent-busy 拒绝:直接留空,不发注定失败的 RPC。
1341
+ detail.model ??= { model: "", reasoning: "" };
1342
+ } else if (plan.model && typeof api.models === "function") {
1343
+ const promise = enqueueDetailLoad(() => delayedModelCall(() => api.models({ sessionId: id }))
1344
+ .then((response) => {
1345
+ const value = apiValue(response);
1346
+ if (!value) {
1347
+ if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
1348
+ return;
1349
+ }
1350
+ // 目录订阅已产出更新的当前选择时,晚到的 RPC 快照不得回写旧值(R-01-012/AC-16)。
1351
+ if (detail.modelLive) return;
1352
+ detail.models = value;
1353
+ detail.model = modelMetadata(value);
1354
+ })
1355
+ .catch((error) => {
1356
+ if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
1357
+ detail.modelError = error instanceof Error ? error.message : String(error);
1358
+ }));
1359
+ modelLoads.set(id, promise);
1360
+ // settle 即移除记账:在途判定驱动加载指示,残留会让空字段永久误报加载。
1361
+ promise.finally(() => {
1362
+ if (modelLoads.get(id) === promise) modelLoads.delete(id);
1363
+ });
1364
+ modelPromises.push(promise);
1365
+ }
1366
+ if (plan.history && typeof api.history === "function") {
1367
+ if (previewFallbackIds.has(id)) detail.previewFallbackLoaded = true;
1368
+ const promise = enqueueDetailLoad(() => Promise.resolve()
1369
+ .then(async () => {
1370
+ // 单池任务内串行回溯深翻(默认无页数上限):向前翻到命中最近一条
1371
+ // 用户消息或翻尽为止——超长会话的最后用户消息可能在尾页窗口之外,
1372
+ // 固定页数上限会让历史卡用户预览永久缺失(R-01-013/AC-03 回溯承诺)。
1373
+ // 运行会话缺窗口内回合起点时要求深翻至命中开放回合 turn/start
1374
+ // (R-01-009/AC-06 冷窗口兜底)。
1375
+ const { events, error } = await pagedHistoryEvents({
1376
+ fetchPage: async (beforeSeq) => apiValue(await api.history({ sessionId: id, beforeSeq, maxMessages: 50 })),
1377
+ requireOpenTurnStart: turnStartMissing,
1378
+ });
1379
+ if (error) detail.historyError = error instanceof Error ? error.message : String(error);
1380
+ detail.history = events;
1381
+ // R-01-017:冷路径同样折叠分组(取全量页内事件再折成最多 4 组,
1382
+ // 含指令锚行窗口选择,R-01-012/AC-12~AC-15)。
1383
+ detail.timeline = foldedHistoryTimeline(events, 4, byId[id]?.cwd ?? "");
1384
+ detail.previews = messagePreviews({ history: events });
1385
+ }))
1386
+ historyLoads.set(id, promise);
1387
+ promise.finally(() => {
1388
+ if (historyLoads.get(id) === promise) historyLoads.delete(id);
1389
+ });
1390
+ historyPromises.push(promise);
1391
+ }
1392
+ }
1393
+ const pending = modelPromises.concat(historyPromises);
1394
+ if (pending.length > 0) {
1395
+ // 逐个完成即重绘(先就绪先显示,不等待全部,R-01-014/AC-03);
1396
+ // 并立即重绘一次让加载指示在数据返回前出现。
1397
+ for (const promise of pending) promise.then(queueSync, queueSync);
1398
+ queueSync();
1399
+ }
1400
+ }
1401
+
1402
+ function installServiceSubscriptions() {
1403
+ const nextSessions = ctx.get("sessions");
1404
+ const nextWorkspaces = ctx.get("workspaces");
1405
+ if (nextSessions === sessions && nextWorkspaces === workspaces) return;
1406
+
1407
+ sessionUnsubscribe?.();
1408
+ workspaceUnsubscribe?.();
1409
+ sessions = nextSessions ?? null;
1410
+ workspaces = nextWorkspaces ?? null;
1411
+ try {
1412
+ sessionUnsubscribe = sessions?.list?.subscribe?.(queueSync) ?? null;
1413
+ } catch {
1414
+ sessionUnsubscribe = null;
1415
+ }
1416
+ try {
1417
+ workspaceUnsubscribe = workspaces?.list?.subscribe?.(queueSync) ?? null;
1418
+ } catch {
1419
+ workspaceUnsubscribe = null;
1420
+ }
1421
+
1422
+ queueSync();
1423
+ }
1424
+
1425
+ function togglePane(open) {
1426
+ const pane = document.querySelector(`[${PANE_ATTR}]`);
1427
+ if (pane === null) return;
1428
+ // 开合状态单点写入:同步抽屉滑入、透明遮罩显隐与浮动开关显隐
1429
+ //(R-01-008/AC-03、AC-05)。
1430
+ pane.setAttribute("data-open", open ? "true" : "false");
1431
+ backdrop.toggleAttribute("data-drawer-open", open);
1432
+ toggle.toggleAttribute("data-drawer-open", open);
1433
+ queueSync();
1434
+ }
1435
+ function notifyLayoutChange() {
1436
+ try {
1437
+ window.dispatchEvent(new Event("resize"));
1438
+ } catch {}
1439
+ }
1440
+ function bindPaneControls(pane) {
1441
+ const header = pane.querySelector(".dap-header");
1442
+ const rail = pane.querySelector(".dap-rail");
1443
+ const resize = pane.querySelector(".dap-resize");
1444
+ const scroll = pane.querySelector(".dap-scroll");
1445
+ const topBtn = pane.querySelector(".dap-top");
1446
+ // 标题行整体即收起控件,两端断点一致:桌面折叠为窄条(R-01-011/AC-03);
1447
+ // 移动端断点解释为收起抽屉,而非折叠窄条(R-01-008/AC-02、R-01-011/AC-06)。
1448
+ const onHeaderActivate = () => {
1449
+ if (window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT})`).matches) {
1450
+ togglePane(false);
1451
+ return;
1452
+ }
1453
+ collapsed = true;
1454
+ pane.setAttribute("data-collapsed", "true");
1455
+ queueSync();
1456
+ notifyLayoutChange();
1457
+ };
1458
+ // Enter/Space 键盘激活与 click 同路径(R-01-011/AC-03、R-01-008/AC-02)。
1459
+ const onHeaderKeydown = (event) => {
1460
+ if (event.key !== "Enter" && event.key !== " ") return;
1461
+ event.preventDefault();
1462
+ onHeaderActivate();
1463
+ };
1464
+ // 「回到顶部」按钮显隐单点(R-01-018/AC-01、AC-03):scrollTop 超阈值显示、阈值内隐藏。
1465
+ const syncTopBtn = () => {
1466
+ if (topBtn !== null && scroll !== null) topBtn.hidden = scroll.scrollTop <= TOP_THRESHOLD;
1467
+ };
1468
+ const onRailClick = () => {
1469
+ collapsed = false;
1470
+ pane.setAttribute("data-collapsed", "false");
1471
+ queueSync();
1472
+ notifyLayoutChange();
1473
+ // 折叠期间 display:none 可能令 scrollTop 归零而不派发 scroll 事件,展开时同步一次。
1474
+ syncTopBtn();
1475
+ };
1476
+ // 桌面拖拽调宽(R-01-015):pointer capture 跟踪 pointermove 实时夹取写入
1477
+ // --dap-width(主会话经 flex 弹性同步让位),抬起/取消时持久化并通知布局。
1478
+ const onResizeDown = (event) => {
1479
+ if (event.button !== 0) return;
1480
+ event.preventDefault();
1481
+ const startX = event.clientX;
1482
+ const startWidth = pane.getBoundingClientRect().width;
1483
+ // 拖拽期间经 rAF 合帧派发 resize,依赖该事件的 sibling overlay 实时跟随让位。
1484
+ let resizeNotifyHandle = null;
1485
+ const onResizeMove = (move) => {
1486
+ paneWidth = clampPaneWidth(startWidth + move.clientX - startX);
1487
+ pane.style.setProperty("--dap-width", `${paneWidth}px`);
1488
+ if (resizeNotifyHandle === null) {
1489
+ resizeNotifyHandle = requestAnimationFrame(() => {
1490
+ resizeNotifyHandle = null;
1491
+ notifyLayoutChange();
1492
+ });
1493
+ }
1494
+ };
1495
+ const onResizeUp = () => {
1496
+ resize.removeAttribute("data-dragging");
1497
+ resize.removeEventListener("pointermove", onResizeMove);
1498
+ resize.removeEventListener("pointerup", onResizeUp);
1499
+ resize.removeEventListener("pointercancel", onResizeUp);
1500
+ if (resizeNotifyHandle !== null) cancelAnimationFrame(resizeNotifyHandle);
1501
+ writeStoredPaneWidth(paneWidth);
1502
+ notifyLayoutChange();
1503
+ };
1504
+ resize.setAttribute("data-dragging", "");
1505
+ resize.addEventListener("pointermove", onResizeMove);
1506
+ resize.addEventListener("pointerup", onResizeUp);
1507
+ resize.addEventListener("pointercancel", onResizeUp);
1508
+ resize.setPointerCapture(event.pointerId);
1509
+ };
1510
+ // 滚动条仅滚动时显示(R-01-004/AC-03):滚动即置位,停滚 600ms 后隐藏。
1511
+ // 同一监听承载「回到顶部」按钮显隐(R-01-018/AC-01、AC-03):回顶后的收口不经额外事件,
1512
+ // 由平滑滚动触发的 scroll 事件自然完成。
1513
+ let scrollHideTimer = null;
1514
+ const onScroll = () => {
1515
+ queueTrackSync(); // 滚动停在小数相位后重对齐轨道层(rAF 合帧)
1516
+ scroll.setAttribute("data-scrolling", "");
1517
+ syncTopBtn();
1518
+ if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
1519
+ scrollHideTimer = setTimeout(() => {
1520
+ scrollHideTimer = null;
1521
+ scroll.removeAttribute("data-scrolling");
1522
+ }, 600);
1523
+ };
1524
+ // 「回到顶部」激活(R-01-018/AC-02):reduced-motion 直接定位,否则平滑滚动;
1525
+ // 原生 <button> 的 Enter/Space 键盘激活与 click 同路径。
1526
+ const onTopClick = () => {
1527
+ scroll?.scrollTo({ top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth" });
1528
+ };
1529
+ header?.addEventListener("click", onHeaderActivate);
1530
+ header?.addEventListener("keydown", onHeaderKeydown);
1531
+ rail?.addEventListener("click", onRailClick);
1532
+ scroll?.addEventListener("scroll", onScroll, { passive: true });
1533
+ topBtn?.addEventListener("click", onTopClick);
1534
+ resize?.addEventListener("pointerdown", onResizeDown);
1535
+ return () => {
1536
+ header?.removeEventListener("click", onHeaderActivate);
1537
+ header?.removeEventListener("keydown", onHeaderKeydown);
1538
+ rail?.removeEventListener("click", onRailClick);
1539
+ scroll?.removeEventListener("scroll", onScroll);
1540
+ if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
1541
+ topBtn?.removeEventListener("click", onTopClick);
1542
+ resize?.removeEventListener("pointerdown", onResizeDown);
1543
+ };
1544
+ }
1545
+
1546
+ // ---- 窗格容器(conversation 槽座的前置兄弟列;外壳重挂载后重插) ----
1547
+ function ensurePane() {
1548
+ const seat = document.querySelector(CONVERSATION_SELECTOR);
1549
+ if (seat === null || seat.parentElement === null) return null;
1550
+ // [data-slot="conversation"] 的父级是 AppFrame 的中间列(flex column)。
1551
+ // 窗格作为该父级的真实 flex 子项、插在会话座之前(行方向 → 窗格在左),
1552
+ // 与其它元素一起参与布局,而不是浮层。
1553
+ const center = seat.parentElement;
1554
+ let pane = center.querySelector(`[${PANE_ATTR}]`);
1555
+ if (pane === null) {
1556
+ pane = document.createElement("aside");
1557
+ pane.setAttribute(PANE_ATTR, "");
1558
+ pane.className = PANE_CLASS;
1559
+ center.insertBefore(pane, seat);
1560
+ pane.innerHTML = `
1561
+ <div class="dap-header" role="button" tabindex="0" aria-expanded="true" aria-label="收起活动会话窗格" title="收起">
1562
+ <span>活动会话</span>
1563
+ <span class="dap-count" role="status" aria-live="polite"></span>
1564
+ <span class="dap-collapse-hint" aria-hidden="true">«</span>
1565
+ </div>
1566
+ <div class="dap-scroll">
1567
+ <div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
1568
+ <div class="dap-recent">
1569
+ <div class="dap-recent-head"><span>最近历史 · 24h</span></div>
1570
+ </div>
1571
+ </div>
1572
+ <button class="dap-top" type="button" aria-label="回到顶部" title="回到顶部" hidden></button>
1573
+ <button class="dap-rail" type="button" aria-label="展开活动会话窗格">
1574
+ <span class="dap-rail-title" aria-hidden="true">活动会话</span>
1575
+ <span class="dap-rail-count" role="status" aria-live="polite"></span>
1576
+ </button>
1577
+ <div class="dap-resize" aria-hidden="true"></div>
1578
+ `;
1579
+ pane.style.setProperty("--dap-width", `${paneWidth}px`);
1580
+ // 「回到顶部」按钮为纯图标呈现(R-01-018/AC-05):骨架无文字,图标在创建时注入。
1581
+ pane.querySelector(".dap-top").append(createTopIcon());
1582
+ }
1583
+ if (pane !== boundPane) {
1584
+ unbindPaneControls?.();
1585
+ unbindPaneControls = bindPaneControls(pane);
1586
+ boundPane = pane;
1587
+ }
1588
+ return pane;
1589
+ }
1590
+
1591
+ function makeEl(tag, cls) {
1592
+ const node = document.createElement(tag);
1593
+ if (cls) node.className = cls;
1594
+ return node;
1595
+ }
1596
+
1597
+ /** 完成提醒卡「移入历史」按钮(R-01-002/AC-10,C-040):默认隐藏,仅完成提醒卡显示;
1598
+ * 文案本身即可访问名称;点击与键盘激活均不得触发卡片跳转(激活锚点在渲染期绑定)。 */
1599
+ function makeConfirmButton() {
1600
+ const button = document.createElement("button");
1601
+ button.type = "button";
1602
+ button.className = "dap-confirm";
1603
+ button.hidden = true;
1604
+ button.textContent = "移入历史";
1605
+ return button;
1606
+ }
1607
+
1608
+ /** 将结构化提问预览写成原生列表元素;动态文本只经 textContent 写入(R-01-002/AC-09)。 */
1609
+ function renderQuestionPreview(note, preview) {
1610
+ const items = Array.isArray(preview?.items)
1611
+ ? preview.items.filter((item) => Number.isInteger(item?.index) && item.index > 0 && typeof item.text === "string" && item.text !== "")
1612
+ : [];
1613
+ if (items.length === 0) return false;
1614
+ const ordered = items.length > 1;
1615
+ const list = makeEl(ordered ? "ol" : "ul", "dap-question-list");
1616
+ for (const item of items) {
1617
+ const itemEl = document.createElement("li");
1618
+ if (ordered) itemEl.value = item.index;
1619
+ itemEl.textContent = item.text;
1620
+ list.append(itemEl);
1621
+ }
1622
+ if (preview.omitted === true) {
1623
+ const omitted = makeEl("li", "dap-question-ellipsis");
1624
+ omitted.textContent = "…";
1625
+ list.append(omitted);
1626
+ }
1627
+ note.replaceChildren(list);
1628
+ return true;
1629
+ }
1630
+
1631
+ /** 陈旧等待卡骨架就地迁移(C-043 热装兼容):旧版末行为「正文行 + 行尾类型徽标」单行,
1632
+ * 新版为「胶囊行 + 正文行」两段——把旧 noteRow 移入新的 .dap-foot 包裹并去掉行尾徽标,
1633
+ * confirm 按钮节点复用(已绑定的 ack 监听不丢)。 */
1634
+ function migrateAwaitingFoot(el) {
1635
+ const noteRow = el.querySelector(".dap-note-row");
1636
+ if (noteRow === null) return;
1637
+ noteRow.querySelector(".dap-badge")?.remove();
1638
+ const capsule = makeEl("div", "dap-capsule");
1639
+ capsule.append(makeEl("span", "dap-capsule-icon"), makeEl("span", "dap-capsule-text"));
1640
+ const foot = makeEl("div", "dap-foot");
1641
+ noteRow.replaceWith(foot);
1642
+ foot.append(capsule, noteRow);
1643
+ }
1644
+
1645
+ /** 静态骨架卡片;动态文本一律走 textContent,规避 HTML 注入。 */
1646
+ function cardChildren(kind) {
1647
+ const head = makeEl("div", "dap-card-head");
1648
+ const workspace = makeEl("div", "dap-workspace");
1649
+ const workspaceIcon = makeEl("span", "dap-workspace-icon");
1650
+ workspaceIcon.append(createWorkspaceFolderIcon());
1651
+ workspace.append(workspaceIcon, makeEl("span", "dap-workspace-text"));
1652
+ head.append(workspace, makeEl("div", "dap-model"));
1653
+ if (kind === "subagent") {
1654
+ const row = makeEl("div", "dap-row");
1655
+ row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
1656
+ return [head, row, makeEl("div", "dap-subtrace")];
1657
+ }
1658
+ if (kind === "recent") {
1659
+ const row = makeEl("div", "dap-row");
1660
+ row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
1661
+ const userLine = makeEl("div", "dap-history-line");
1662
+ userLine.dataset.role = "user";
1663
+ const userIcon = makeEl("span", "dap-history-icon");
1664
+ userIcon.append(createUserIcon());
1665
+ const userLabel = makeEl("span", "dap-history-label");
1666
+ userLabel.textContent = "用户";
1667
+ userLine.append(userIcon, userLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
1668
+ const agentLine = makeEl("div", "dap-history-line");
1669
+ agentLine.dataset.role = "agent";
1670
+ const agentIcon = makeEl("span", "dap-history-icon");
1671
+ agentIcon.append(createRobotIcon());
1672
+ const agentLabel = makeEl("span", "dap-history-label");
1673
+ agentLabel.textContent = "助手";
1674
+ agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
1675
+ return [head, row, userLine, agentLine, makeEl("div", "dap-note")];
1676
+ }
1677
+ if (kind === "awaiting") {
1678
+ const row = makeEl("div", "dap-row");
1679
+ row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
1680
+ // 等待三类末行结构(R-01-002/AC-08、AC-09,C-043):首行「类型胶囊」(圆底类型
1681
+ // 图标 + 类型文字),其下为正文行——阻塞/错误的说明文字,完成提醒的
1682
+ // 「继续对话,或移入历史」+ 行尾「移入历史」按钮。
1683
+ const capsule = makeEl("div", "dap-capsule");
1684
+ capsule.append(makeEl("span", "dap-capsule-icon"), makeEl("span", "dap-capsule-text"));
1685
+ // 正文行容器:正文文本 + 「移入历史」按钮(仅完成提醒卡显示,R-01-002/AC-10)。
1686
+ const noteRow = makeEl("div", "dap-note-row");
1687
+ noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
1688
+ const foot = makeEl("div", "dap-foot");
1689
+ foot.append(capsule, noteRow);
1690
+ return [head, row, makeEl("div", "dap-trace"), foot];
1691
+ }
1692
+ const row = makeEl("div", "dap-row");
1693
+ row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
1694
+ const track = makeEl("div", "dap-track");
1695
+ track.append(makeEl("div", "dap-fill"));
1696
+ const progressRow = makeEl("div", "dap-progress");
1697
+ progressRow.append(track, makeEl("span", "dap-pct"));
1698
+ // 统计行双段结构:左列 token/速率/命中率(超长省略号截断),时长固定最右(R-01-009/AC-05)。
1699
+ const statsRow = makeEl("div", "dap-token-stats");
1700
+ statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
1701
+ statsRow.hidden = true;
1702
+ return [head, row, makeEl("div", "dap-trace"), progressRow, statsRow];
1703
+ }
1704
+
1705
+ function createInlineIcon({ viewBox, width = 12, height = 12, parts }) {
1706
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1707
+ svg.setAttribute("viewBox", viewBox);
1708
+ svg.setAttribute("width", String(width));
1709
+ svg.setAttribute("height", String(height));
1710
+ svg.setAttribute("fill", "none");
1711
+ svg.setAttribute("aria-hidden", "true");
1712
+ for (const part of parts) {
1713
+ const node = document.createElementNS("http://www.w3.org/2000/svg", part.tag ?? "path");
1714
+ for (const [name, value] of Object.entries(part.attrs)) node.setAttribute(name, value);
1715
+ svg.append(node);
1716
+ }
1717
+ return svg;
1718
+ }
1719
+
1720
+ /** 「回到顶部」按钮的向上箭头图标(canonical 图标集无现成箭头;
1721
+ * 几何改编自 ISC 许可证的 Lucide arrow-up,归一到 14 盒——留白 24 盒由
1722
+ * R-01-012/AC-11 的机器人图标守卫全局禁用,与 C-021 同一来源声明)。 */
1723
+ function createTopIcon() {
1724
+ return createInlineIcon({
1725
+ viewBox: "0 0 14 14",
1726
+ width: 14,
1727
+ height: 14,
1728
+ parts: [
1729
+ { attrs: { d: "M7 12.5V2", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
1730
+ { attrs: { d: "m2.5 6.5 4.5-4.5 4.5 4.5", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
1731
+ ],
1732
+ });
1733
+ }
1734
+
1735
+ function createUserIcon() {
1736
+ return createInlineIcon({
1737
+ viewBox: "0 0 16 16",
1738
+ parts: [
1739
+ { attrs: { d: "M11.0307 5.46369C11.0305 3.78995 9.6734 2.43357 7.99961 2.43357C6.32601 2.43379 4.96972 3.79009 4.96949 5.46369C4.96949 7.13748 6.32587 8.49455 7.99961 8.49477C9.67354 8.49477 11.0307 7.13762 11.0307 5.46369ZM12.3163 5.46369C12.3163 7.84777 10.3837 9.78042 7.99961 9.78042C5.61572 9.7802 3.68288 7.84763 3.68288 5.46369C3.6831 3.07993 5.61586 1.14718 7.99961 1.14695C10.3836 1.14695 12.3161 3.0798 12.3163 5.46369Z", fill: "currentColor" } },
1740
+ { attrs: { d: "M8.00002 10.3316C11.7343 10.3316 14.1864 11.8997 15.0387 14.4445L14.4292 14.6483L13.8197 14.8531C13.1955 12.9893 11.3673 11.6182 8.00002 11.6182C4.63277 11.6182 2.80455 12.9893 2.18031 14.8531L1.5708 14.6483L0.961304 14.4445C1.81368 11.8997 4.26579 10.3316 8.00002 10.3316Z", fill: "currentColor" } },
1741
+ ],
1742
+ });
1743
+ }
1744
+
1745
+ /** 左边栏工作区行同款 canonical 文件夹图标(dsh-client-ui-primitives 的 IconFolderClose16),
1746
+ * 置于工作区徽标文字前使归属一眼可辨(R-01-003/AC-06)。 */
1747
+ function createWorkspaceFolderIcon() {
1748
+ return createInlineIcon({
1749
+ viewBox: "0 0 16 16",
1750
+ parts: [
1751
+ { attrs: { transform: "translate(1.5 2.429)", d: "M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z", fill: "currentColor" } },
1752
+ ],
1753
+ });
1754
+ }
1755
+
1756
+ /** DSH canonical IconApiOutline14, used when the native row is unavailable/errored. */
1757
+ function createBashIcon() {
1758
+ return createInlineIcon({
1759
+ viewBox: "0 0 14 14",
1760
+ parts: [
1761
+ { attrs: { transform: "translate(0.6689 1.073)", 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.46965 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.8562 2.91638 11.7723C2.21157 11.6867 1.61933 11.5049 1.12708 11.0845C0.99901 10.975 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-7 7.37222 6.47206e-7 6.2771L6.47206e-7 5.57813C6.47206e-7 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-7 5.57813 6.47206e-7L7.0846 6.47206e-7C8.17999 6.47206e-7 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", fill: "currentColor" } },
1762
+ { attrs: { transform: "translate(0.6689 1.073)", 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", fill: "currentColor" } },
1763
+ { attrs: { transform: "translate(0.6689 1.073)", d: "M9.23789 7.35397L9.23789 8.53488L6.96238 8.53488L6.96238 7.35397L9.23789 7.35397Z", fill: "currentColor" } },
1764
+ ],
1765
+ });
1766
+ }
1767
+
1768
+ /** 以下 canonical 图标与主会话网页同源(dsh-client-ui-primitives 的 figma extract),
1769
+ * 供非当前会话/无原生行时 fallback 使用,保证选中态切换不漂移(R-01-012/AC-03)。 */
1770
+ function createSearchIcon() {
1771
+ return createInlineIcon({
1772
+ viewBox: "0 0 16 16",
1773
+ parts: [
1774
+ { attrs: { d: "M11.894845 6.647401C11.894845 3.725463 9.534486 1.356779 6.623219 1.35657C3.711786 1.35657 1.351635 3.725338 1.351635 6.647401C1.351843 9.569296 3.711911 11.938273 6.623219 11.938273C9.534361 11.938064 11.894637 9.569171 11.894845 6.647401ZM13.245462 6.647401C13.245254 10.317935 10.280401 13.293613 6.623219 13.293821C2.965871 13.293821 0.000204 10.31806 0 6.647401C0 2.976574 2.965746 0 6.623219 0C10.280526 0.000205 13.245462 2.9767 13.245462 6.647401Z", fill: "currentColor" } },
1775
+ { attrs: { d: "M16.000417 15.041079L15.044449 16.000433L11.530434 12.473588L12.486298 11.514234L16.000417 15.041079Z", fill: "currentColor" } },
1776
+ ],
1777
+ });
1778
+ }
1779
+
1780
+ function createBrowseIcon() {
1781
+ return createInlineIcon({
1782
+ viewBox: "0 0 16 16",
1783
+ parts: [
1784
+ { attrs: { d: "M11.2426 4.80473V6.10551H4.75819V4.80473H11.2426Z", fill: "currentColor" } },
1785
+ { attrs: { d: "M9.40858 7.84478V9.14557H4.75819V7.84478H9.40858Z", fill: "currentColor" } },
1786
+ { attrs: { 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", fill: "currentColor" } },
1787
+ ],
1788
+ });
1789
+ }
1790
+
1791
+ function createEditIcon() {
1792
+ return createInlineIcon({
1793
+ viewBox: "0 0 16 16",
1794
+ parts: [
1795
+ { attrs: { d: "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", fill: "currentColor" } },
1796
+ ],
1797
+ });
1798
+ }
1799
+
1800
+ function createCodeIcon() {
1801
+ return createInlineIcon({
1802
+ viewBox: "0 0 16 16",
1803
+ parts: [
1804
+ { attrs: { d: "M12.3368 1.53569L11.931 4.43172H14.8086V5.79673H11.7404L11.1962 9.67859H14.2839V11.0436H11.0056L10.4994 14.6529L9.14873 14.4643L9.62731 11.0436H5.75876L5.25252 14.6529L3.90186 14.4643L4.38043 11.0436H1.69141V9.67859H4.57104L5.11417 5.79673H2.21609V4.43172H5.30581L5.73724 1.34713L7.08995 1.53569L6.68414 4.43172H10.5527L10.9841 1.34713L12.3368 1.53569ZM5.94937 9.67859H9.81791L10.361 5.79673H6.49353L5.94937 9.67859Z", fill: "currentColor", "fill-rule": "evenodd", "clip-rule": "evenodd" } },
1805
+ ],
1806
+ });
1807
+ }
1808
+
1809
+ function createSparkleIcon() {
1810
+ return createInlineIcon({
1811
+ viewBox: "0 0 16 16",
1812
+ parts: [
1813
+ { attrs: { d: "M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z", fill: "currentColor" } },
1814
+ { attrs: { d: "M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z", fill: "currentColor" } },
1815
+ { attrs: { d: "M12.5 9.4Q12.7 11.4 14.7 11.6Q12.7 11.8 12.5 13.8Q12.3 11.8 10.3 11.6Q12.3 11.4 12.5 9.4Z", fill: "currentColor" } },
1816
+ ],
1817
+ });
1818
+ }
1819
+
1820
+ /** agent 角色机器人图标(R-01-013/AC-08、R-01-012/AC-09):canonical 图标集无现成机器人,
1821
+ * 以 Lucide bot 图标几何(ISC 许可,来源声明见 LICENSE/README)为底改造,24 框描边风格:
1822
+ * 去双耳、头顶单折线天线改为两条外撇斜短线(bilibili 小电视式);viewBox 保持笔墨边界盒
1823
+ * (1 3 22 18)——纵向墨迹 y4..21(短斜天线尖含描边触 y4),横向虽去耳收窄,保框使机身
1824
+ * 显示尺度与其他 canonical 图标一致(R-01-012/AC-11),不因裁小框而变粗变大。
1825
+ * 字形与其他时间线图标同用 12px 盒:13px 盒在 14px 图标盒内产生 0.5px 半像素居中偏移,
1826
+ * 所有描边边缘落在物理像素之间被抗锯齿抹灰;stroke-width 2.2 使 12/22 缩放后渲染笔触
1827
+ * 保持 1.2px,与 canonical 填充轮廓环(约 1px)视觉重量相当。 */
1828
+ function createRobotIcon() {
1829
+ const stroke = { fill: "none", stroke: "currentColor", "stroke-width": "2.2", "stroke-linecap": "round", "stroke-linejoin": "round" };
1830
+ return createInlineIcon({
1831
+ viewBox: "1 3 22 18",
1832
+ parts: [
1833
+ { attrs: { d: "M10 8L7 5M14 8L17 5", ...stroke } },
1834
+ { tag: "rect", attrs: { x: "4", y: "8", width: "16", height: "12", rx: "2", ...stroke } },
1835
+ { attrs: { d: "M15 13v2M9 13v2", ...stroke } },
1836
+ ],
1837
+ });
1838
+ }
1839
+
1840
+ function createGlobeIcon() {
1841
+ return createInlineIcon({
1842
+ viewBox: "0 0 14 14",
1843
+ parts: [
1844
+ { attrs: { d: "M7.00018 0.353516C10.6708 0.353535 13.6468 3.32958 13.6469 7.00018C13.6468 10.6708 10.6708 13.6468 7.00018 13.6469C3.32957 13.6468 0.353535 10.6708 0.353516 7.00018C0.353535 3.32957 3.32957 0.353531 7.00018 0.353516ZM5.44643 7.59661C5.49463 8.97506 5.70762 10.191 6.02136 11.0793C6.20141 11.5891 6.40328 11.9585 6.59898 12.1889C6.79501 12.4196 6.93213 12.454 7.00018 12.454C7.06822 12.454 7.20533 12.4197 7.40138 12.1889C7.59708 11.9585 7.79895 11.589 7.979 11.0793C8.29274 10.191 8.50574 8.97506 8.55394 7.59661H5.44643ZM1.57861 7.59661C1.80785 9.70467 3.2386 11.4509 5.1715 12.1388C5.07135 11.9317 4.97972 11.7098 4.89746 11.477C4.53084 10.4391 4.30224 9.0828 4.25357 7.59661H1.57861ZM9.74679 7.59661C9.69813 9.0828 9.46952 10.4391 9.1029 11.477C9.0206 11.7099 8.92818 11.9316 8.82797 12.1388C10.7613 11.4511 12.1925 9.70496 12.4218 7.59661H9.74679ZM5.1706 1.8616C3.23814 2.54963 1.80876 4.29604 1.5795 6.40376H4.25357C4.30224 4.91756 4.53083 3.56129 4.89746 2.5234C4.97968 2.29066 5.07051 2.0686 5.1706 1.8616ZM7.00018 1.54637C6.93213 1.54638 6.79503 1.5807 6.59898 1.81145C6.40332 2.04177 6.20139 2.41058 6.02136 2.92012C5.70754 3.80851 5.49461 5.02499 5.44643 6.40376H8.55394C8.50575 5.025 8.29282 3.80851 7.979 2.92012C7.79898 2.41059 7.59705 2.04177 7.40138 1.81145C7.20531 1.58067 7.06823 1.54637 7.00018 1.54637ZM8.82887 1.8616C8.92902 2.0687 9.02064 2.29053 9.1029 2.5234C9.46953 3.56129 9.69812 4.91756 9.74679 6.40376H12.4209C12.1916 4.29575 10.7618 2.54943 8.82887 1.8616Z", fill: "currentColor", "fill-rule": "evenodd", "clip-rule": "evenodd" } },
1845
+ ],
1846
+ });
1847
+ }
1848
+
1849
+ function createThinkIcon() {
1850
+ return createInlineIcon({
1851
+ viewBox: "0 0 14 14",
1852
+ parts: [
1853
+ { attrs: { 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", fill: "currentColor" } },
1854
+ { attrs: { 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", fill: "currentColor", "fill-rule": "evenodd", "clip-rule": "evenodd" } },
1855
+ ],
1856
+ });
1857
+ }
1858
+
1859
+ /* 胶囊类型种类(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043、C-064):阻塞待确认/待审查/提问、
1860
+ * 完成提醒(对勾)与错误提醒(感叹号);未知种类不给图标(不冒充已知类型)。 */
1861
+ const CAPSULE_ICON_KINDS = new Set(["approval", "plan-review", "question", "done", "error"]);
1862
+
1863
+ /** 阻塞等待徽标类型图标(R-01-002/AC-01、AC-02):通用几何自绘(对勾/文档/问号气泡),
1864
+ * stroke 风格与机器人图标一致;16 框、12px 字形盒,与卡片其它图标尺度对齐。 */
1865
+ function createPendingIcon(kind) {
1866
+ const stroke = { fill: "none", stroke: "currentColor", "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round" };
1867
+ if (kind === "approval")
1868
+ return createInlineIcon({
1869
+ viewBox: "0 0 16 16",
1870
+ parts: [{ attrs: { d: "M3.5 8.8L6.8 12.1L12.6 4.3", ...stroke } }],
1871
+ });
1872
+ if (kind === "plan-review")
1873
+ return createInlineIcon({
1874
+ viewBox: "0 0 16 16",
1875
+ parts: [
1876
+ { tag: "rect", attrs: { x: "4", y: "2.5", width: "8", height: "11", rx: "1.5", ...stroke } },
1877
+ { attrs: { d: "M6.3 6.5h3.4M6.3 9.2h3.4", ...stroke } },
1878
+ ],
1879
+ });
1880
+ // question:问号气泡(圆 + 问号钩 + 圆点)。
1881
+ return createInlineIcon({
1882
+ viewBox: "0 0 16 16",
1883
+ parts: [
1884
+ { tag: "circle", attrs: { cx: "8", cy: "8", r: "5.9", ...stroke } },
1885
+ { attrs: { d: "M6.6 6.1c.25-1.05 1.05-1.65 1.95-1.65 1.05 0 1.85.7 1.85 1.65 0 1.25-1.25 1.55-1.85 2.3v.35", ...stroke } },
1886
+ { attrs: { d: "M8.55 11.4v.5", ...stroke } },
1887
+ ],
1888
+ });
1889
+ }
1890
+
1891
+ /** 等待卡类型胶囊图标(R-01-002/AC-09、AC-13,C-043):done 复用对勾几何(与待确认同形、
1892
+ * 靠胶囊与卡面颜色区分成功/确认语义),error 为感叹号气泡(圆 + 感叹号钩 + 圆点);
1893
+ * 其余走阻塞等待图标。 */
1894
+ function createCapsuleIcon(kind) {
1895
+ const stroke = { fill: "none", stroke: "currentColor", "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round" };
1896
+ if (kind === "done")
1897
+ return createInlineIcon({
1898
+ viewBox: "0 0 16 16",
1899
+ parts: [{ attrs: { d: "M3.5 8.8L6.8 12.1L12.6 4.3", ...stroke } }],
1900
+ });
1901
+ if (kind === "error")
1902
+ return createInlineIcon({
1903
+ viewBox: "0 0 16 16",
1904
+ parts: [
1905
+ { tag: "circle", attrs: { cx: "8", cy: "8", r: "5.9", ...stroke } },
1906
+ { attrs: { d: "M8 5.2v3.5", ...stroke } },
1907
+ { attrs: { d: "M8 11.4v.5", ...stroke } },
1908
+ ],
1909
+ });
1910
+ return createPendingIcon(kind);
1911
+ }
1912
+
1913
+ /** fallback 图标分类:先按 toolName 镜像原生 classifyTool 与行级覆盖(web_search 用地球、
1914
+ * cordis 插件动作用 sparkle),未知工具按 view.kind 语义兜底,再兜底原生 others 的 sparkle。 */
1915
+ const TOOL_ICON_FACTORIES = {
1916
+ bash: createBashIcon,
1917
+ pwsh: createBashIcon,
1918
+ read: createBrowseIcon,
1919
+ web_fetch: createBrowseIcon,
1920
+ cordis_package_inspect: createBrowseIcon,
1921
+ cordis_runtime_inspect: createBrowseIcon,
1922
+ web_search: createGlobeIcon,
1923
+ grep: createSearchIcon,
1924
+ glob: createSearchIcon,
1925
+ write: createEditIcon,
1926
+ edit: createEditIcon,
1927
+ run_code: createCodeIcon,
1928
+ cordis_run: createSparkleIcon,
1929
+ cordis_stop: createSparkleIcon,
1930
+ cordis_undefine: createSparkleIcon,
1931
+ };
1932
+ const KIND_ICON_FACTORIES = {
1933
+ read: createBrowseIcon,
1934
+ fetch: createBrowseIcon,
1935
+ search: createSearchIcon,
1936
+ bash: createBashIcon,
1937
+ execute: createBashIcon,
1938
+ edit: createEditIcon,
1939
+ write: createEditIcon,
1940
+ code: createCodeIcon,
1941
+ };
1942
+ function fallbackTraceIcon(item) {
1943
+ // 折叠组行(R-01-017):think 组用思考图标、context 组用浏览图标;tool 组按末位工具成员 icon 兜底。
1944
+ // 折叠组行(R-01-017):think 组用思考图标、context 组用浏览图标;
1945
+ // tool 组统一 DSH canonical IconApiOutline14(createBashIcon),与 auto-collapse 工具 chip 同源(C-016)。
1946
+ if (item.fold === true) {
1947
+ if (item.kind === "assistant") return createThinkIcon();
1948
+ if (item.kind === "context") return createBrowseIcon();
1949
+ return createBashIcon();
1950
+ }
1951
+
1952
+ if (item.kind === "user") return createUserIcon();
1953
+ // 与 core 思考语义判定同源的 truthy 谓词:detail(reasoning)存在即思考行,否则正文行。
1954
+ if (item.kind === "assistant") return item.detail ? createThinkIcon() : createRobotIcon();
1955
+ if (item.kind === "context") return createBrowseIcon();
1956
+ return (TOOL_ICON_FACTORIES[item.toolName] ?? KIND_ICON_FACTORIES[item.icon] ?? createSparkleIcon)();
1957
+ }
1958
+
1959
+
1960
+ /**
1961
+ * 更新 trace 容器。运行中的同一节点只更新文字,复用 DOM 保持脉冲动画连续;
1962
+ * 节点身份或数量变化时才重建(R-02-003/AC-01)。
1963
+ */
1964
+ function renderTrace(container, items, { lastOnly = false } = {}) {
1965
+ const list = Array.isArray(items) ? items : [];
1966
+ const sources = lastOnly ? (list.length === 0 ? [] : [list[list.length - 1]]) : list;
1967
+ const valid = sources.filter((item) => item !== null && typeof item === "object");
1968
+ const existing = Array.from(container.children);
1969
+ const stable =
1970
+ existing.length === valid.length &&
1971
+ existing.every((line, index) => line.dataset.traceKey === String(valid[index].id ?? index));
1972
+ const lines = stable ? existing : [];
1973
+ if (!stable) container.replaceChildren();
1974
+ for (let index = 0; index < valid.length; index += 1) {
1975
+ const item = valid[index];
1976
+ const key = String(item.id ?? index);
1977
+ const line = lines[index] ?? makeEl("div", "dap-trace-item");
1978
+ line.dataset.traceKey = key;
1979
+ // 核心派生状态直接上卡:显示行全部由核心折叠派生,无原生 data-state 合并维度。
1980
+ line.dataset.status = item.status ?? "running";
1981
+ // data-icon 供 CSS 区分呈现:机器人正文行单设 "robot"(思考组/思考行 icon 同为 "assistant" 但渲染
1982
+ // 思考图标,不用机器人圆底;图标圆底仅限用户指令行与助手正文行)。
1983
+ line.dataset.icon =
1984
+ item.kind === "assistant" && item.fold !== true && !item.detail
1985
+ ? "robot"
1986
+ : typeof item.icon === "string" ? item.icon : "other";
1987
+ // 活动流式更新只改文本,保留命中节点;按下/抬起之间替换子节点会让浏览器取消 click。
1988
+ let main = line.querySelector(".dap-trace-main");
1989
+ if (main === null) {
1990
+ main = makeEl("span", "dap-trace-main");
1991
+ line.append(main);
1992
+ }
1993
+ const labelText = item.label || "";
1994
+ // context 项的 text 是注入内容原文,不是摘要,禁止兜底上卡。
1995
+ const summaryText = item.summary || item.detail || (item.kind === "context" ? "" : item.text) || "";
1996
+ const structure = [
1997
+ "dap-trace-icon",
1998
+ labelText ? "dap-trace-label" : null,
1999
+ labelText && summaryText ? "dap-trace-separator" : null,
2000
+ summaryText ? "dap-trace-summary" : null,
2001
+ ].filter(Boolean);
2002
+ const currentStructure = [...main.children].map((child) => child.className);
2003
+ const iconKey = JSON.stringify([item.kind ?? "", item.icon ?? "", item.toolName ?? "", item.label ?? "", line.dataset.status]);
2004
+ const paintIcon = (host) => {
2005
+ host.replaceChildren();
2006
+ host.append(fallbackTraceIcon(item));
2007
+ host.dataset.iconKey = iconKey;
2008
+ };
2009
+ let iconHost = main.querySelector(".dap-trace-icon");
2010
+ if (currentStructure.join("\0") !== structure.join("\0")) {
2011
+ main.replaceChildren();
2012
+ iconHost = makeEl("span", "dap-trace-icon");
2013
+ main.append(iconHost);
2014
+ if (labelText) main.append(makeEl("span", "dap-trace-label"));
2015
+ if (labelText && summaryText) main.append(makeEl("span", "dap-trace-separator"));
2016
+ if (summaryText) main.append(makeEl("span", "dap-trace-summary"));
2017
+ }
2018
+ if (iconHost !== null && iconHost.dataset.iconKey !== iconKey) paintIcon(iconHost);
2019
+ const label = main.querySelector(".dap-trace-label");
2020
+ if (label !== null) label.textContent = labelText;
2021
+ const separator = main.querySelector(".dap-trace-separator");
2022
+ if (separator !== null) separator.setAttribute("aria-hidden", "true");
2023
+ const summary = main.querySelector(".dap-trace-summary");
2024
+ if (summary !== null) {
2025
+ // 镜像原生 ReasoningRow follow-end:running 摘要视口钉行尾跟随流式输出,
2026
+ // 否则回行首;仅在文本或跟随态变化时读写 scrollLeft(scrollWidth 读取触发布局)。
2027
+ const follow = line.dataset.status === "running" ? "end" : "start";
2028
+ if (summary.textContent !== summaryText || summary.dataset.follow !== follow) {
2029
+ summary.textContent = summaryText;
2030
+ summary.dataset.follow = follow;
2031
+ summary.scrollLeft = follow === "end" ? summary.scrollWidth : 0;
2032
+ }
2033
+ }
2034
+ const statusLabels = { running: "进行中", done: "已完成", ok: "已完成", error: "失败", stopped: "已停止" };
2035
+ line.setAttribute("aria-label", [labelText, summaryText, statusLabels[line.dataset.status] ?? line.dataset.status].filter(Boolean).join(" · "));
2036
+ if (!stable) container.append(line);
2037
+ }
2038
+ }
2039
+
2040
+ /** 字段写回:离开加载态时无条件写回(spinner 无文本,相等守卫会漏清空值);
2041
+ * 否则只在文本变化时写入(保持 DOM 去重语义)。 */
2042
+ function restoreTextField(el, text) {
2043
+ if (el.dataset.loading === "true") {
2044
+ delete el.dataset.loading;
2045
+ el.textContent = text;
2046
+ } else if (el.textContent !== text) {
2047
+ el.textContent = text;
2048
+ }
2049
+ }
2050
+
2051
+ /** 时间线区加载指示:数据在途且尚无工作项时显示活动图标行(R-01-014/AC-02)。 */
2052
+ function renderTraceLoading(container) {
2053
+ if (container.dataset.loading === "true") return;
2054
+ container.dataset.loading = "true";
2055
+ const row = makeEl("div", "dap-trace-item");
2056
+ row.dataset.status = "running";
2057
+ const main = makeEl("span", "dap-trace-main");
2058
+ const summary = makeEl("span", "dap-trace-summary");
2059
+ summary.textContent = "加载中…";
2060
+ main.append(makeEl("span", "dap-spinner"), summary);
2061
+ row.append(main);
2062
+ container.replaceChildren(row);
2063
+ }
2064
+
2065
+ /** 时间线区统一渲染:在途且无工作项时显示加载行,否则渲染工作项时间线。 */
2066
+ function renderTimelineArea(container, entry, { lastOnly = false } = {}) {
2067
+ if (entry.loadingTimeline === true && entry.timeline.length === 0) {
2068
+ renderTraceLoading(container);
2069
+ return;
2070
+ }
2071
+ if (container.dataset.loading === "true") delete container.dataset.loading;
2072
+ renderTrace(container, entry.timeline, { lastOnly });
2073
+ }
2074
+
2075
+ function renderCardInto(el, entry, hueByWorkspace) {
2076
+ const workspaceLabel = el.querySelector(".dap-workspace");
2077
+ if (workspaceLabel !== null) {
2078
+ const workspaceText = workspaceLabel.querySelector(".dap-workspace-text");
2079
+ if (entry.workspaceTitle !== "") {
2080
+ if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle);
2081
+ const hue = hueByWorkspace.get(entry.workspaceKey) ?? workspaceHue(entry.workspaceKey);
2082
+ const hueText = hue === null ? "" : String(hue);
2083
+ if (workspaceLabel.style.getPropertyValue("--dap-workspace-hue") !== hueText) {
2084
+ if (hue === null) workspaceLabel.style.removeProperty("--dap-workspace-hue");
2085
+ else workspaceLabel.style.setProperty("--dap-workspace-hue", hueText);
2086
+ }
2087
+ workspaceLabel.removeAttribute("hidden");
2088
+ } else {
2089
+ if (workspaceText !== null) restoreTextField(workspaceText, "");
2090
+ workspaceLabel.style.removeProperty("--dap-workspace-hue");
2091
+ workspaceLabel.setAttribute("hidden", "");
2092
+ }
2093
+ }
2094
+ const modelLabel = el.querySelector(".dap-model");
2095
+ if (modelLabel !== null) {
2096
+ if (entry.loadingModel === true) {
2097
+ if (modelLabel.dataset.loading !== "true") {
2098
+ modelLabel.dataset.loading = "true";
2099
+ modelLabel.replaceChildren(makeEl("span", "dap-spinner"));
2100
+ }
2101
+ } else {
2102
+ restoreTextField(modelLabel, [entry.model, entry.reasoning].filter(Boolean).join(" · "));
2103
+ }
2104
+ }
2105
+ const title = el.querySelector(".dap-title");
2106
+ if (title !== null && title.textContent !== entry.title)
2107
+ title.textContent = entry.title;
2108
+
2109
+ const capsule = el.querySelector(".dap-capsule");
2110
+ // 陈旧骨架就地迁移(C-043 热装兼容):旧版「正文行 + 行尾类型徽标」骨架升级为
2111
+ // 「胶囊行 + 正文行」;仅迁移等待卡且确认按钮节点复用(不丢已绑定的 ack 监听)。
2112
+ if (capsule === null && entry.kind === "awaiting" && el.querySelector(".dap-badge") !== null) {
2113
+ migrateAwaitingFoot(el);
2114
+ }
2115
+ if (capsule !== null || entry.kind === "awaiting") {
2116
+ // 胶囊文字:blocked=待确认/待审查/提问中(pendingText),done=「已完成」,error=「错误」。
2117
+ const capsuleText = el.querySelector(".dap-capsule-text");
2118
+ const text =
2119
+ entry.waitClass === "blocked"
2120
+ ? (entry.pendingText ?? "")
2121
+ : entry.waitClass === "done"
2122
+ ? "已完成"
2123
+ : entry.waitClass === "error"
2124
+ ? "错误"
2125
+ : "";
2126
+ if (capsuleText !== null && capsuleText.textContent !== text) capsuleText.textContent = text;
2127
+ // 胶囊前置圆形类型图标:blocked=对勾/文档/问号,done=对勾,error=感叹号。
2128
+ const iconHolder = el.querySelector(".dap-capsule-icon");
2129
+ const iconKind =
2130
+ entry.waitClass === "blocked"
2131
+ ? CAPSULE_ICON_KINDS.has(entry.pendingKind)
2132
+ ? entry.pendingKind
2133
+ : ""
2134
+ : entry.waitClass === "done"
2135
+ ? "done"
2136
+ : entry.waitClass === "error"
2137
+ ? "error"
2138
+ : "";
2139
+ if (iconHolder !== null && (iconHolder.dataset.kind ?? "") !== iconKind) {
2140
+ iconHolder.dataset.kind = iconKind;
2141
+ iconHolder.replaceChildren(...(iconKind === "" ? [] : [createCapsuleIcon(iconKind)]));
2142
+ }
2143
+ }
2144
+
2145
+ if (entry.kind === "running") {
2146
+ const pct = el.querySelector(".dap-pct");
2147
+ if (pct !== null)
2148
+ pct.textContent = `${Math.round(entry.progress ?? 0)}%`;
2149
+ const traceContainer = el.querySelector(".dap-trace");
2150
+ if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
2151
+ const fill = el.querySelector(".dap-fill");
2152
+ if (fill !== null) {
2153
+ const width = `${Math.min(100, Math.max(0, entry.progress ?? 0))}%`;
2154
+ if (fill.style.width !== width) fill.style.width = width;
2155
+ }
2156
+ const stats = el.querySelector(".dap-token-stats");
2157
+ if (stats !== null) {
2158
+ const parts = [];
2159
+ if (Number.isFinite(entry.rateTokS) && entry.rateTokS > 0) parts.push(`${Math.round(entry.rateTokS)} tok/s`);
2160
+ if (Number.isFinite(entry.cacheHitPct)) parts.push(`缓存 ${entry.cacheHitPct}%`);
2161
+ if (Number.isFinite(entry.inputTokens) && entry.inputTokens >= 0) parts.push(`输入 ${fmtTokens(entry.inputTokens) ?? entry.inputTokens}`);
2162
+ if (Number.isFinite(entry.outputTokens) && entry.outputTokens >= 0) parts.push(`输出 ${fmtTokens(entry.outputTokens) ?? entry.outputTokens}`);
2163
+ const mainText = parts.join(" · ");
2164
+ const timeText = Number.isFinite(entry.elapsedMs) && entry.elapsedMs >= 0 ? fmtElapsedMs(entry.elapsedMs) : "";
2165
+ let mainTextEl = stats.querySelector(".dap-token-main");
2166
+ let timeEl = stats.querySelector(".dap-token-time");
2167
+ if (mainTextEl === null || timeEl === null) {
2168
+ // 热装残留的旧版单文本段骨架:就地重建双段结构再写值。
2169
+ mainTextEl = makeEl("span", "dap-token-main");
2170
+ timeEl = makeEl("span", "dap-token-time");
2171
+ stats.replaceChildren(mainTextEl, timeEl);
2172
+ }
2173
+ if (mainTextEl.textContent !== mainText) mainTextEl.textContent = mainText;
2174
+ if (timeEl.textContent !== timeText) timeEl.textContent = timeText;
2175
+ const statsHidden = mainText === "" && timeText === "";
2176
+ if (stats.hidden !== statsHidden) stats.hidden = statsHidden;
2177
+ }
2178
+ return;
2179
+ }
2180
+
2181
+ if (entry.kind === "subagent") {
2182
+ const traceContainer = el.querySelector(".dap-subtrace");
2183
+ if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: true });
2184
+ return;
2185
+ }
2186
+
2187
+ if (entry.kind === "recent") {
2188
+ const lines = el.querySelectorAll(".dap-history-line");
2189
+ const previews = [entry.userPreview ?? "", entry.agentPreview ?? ""];
2190
+ for (let i = 0; i < 2; i += 1) {
2191
+ const line = lines[i];
2192
+ if (!line) continue;
2193
+ const text = line.querySelector(".dap-history-text");
2194
+ if (!text) continue;
2195
+ if (entry.loadingPreviews === true) {
2196
+ if (text.dataset.loading !== "true") {
2197
+ text.dataset.loading = "true";
2198
+ text.replaceChildren(makeEl("span", "dap-spinner"));
2199
+ }
2200
+ } else {
2201
+ restoreTextField(text, previews[i]);
2202
+ }
2203
+ }
2204
+ }
2205
+
2206
+ if (entry.kind === "awaiting") {
2207
+ const traceContainer = el.querySelector(".dap-trace");
2208
+ if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
2209
+ const confirm = el.querySelector(".dap-confirm");
2210
+ if (confirm !== null) {
2211
+ // 激活锚点:只在结构重建时绑一次(卡片按 id 复用,kind 变化会重建骨架)。
2212
+ if (confirm.dataset.bound !== "true") {
2213
+ confirm.dataset.bound = "true";
2214
+ confirm.addEventListener("click", (event) => {
2215
+ event.preventDefault();
2216
+ event.stopPropagation(); // 阻断卡片激活(R-01-002/AC-10)
2217
+ const id = el.dataset.sessionId;
2218
+ if (id) ackCompletion(id);
2219
+ });
2220
+ // Enter/Space 由按钮原生合成 click;此处阻断其冒泡到卡片 keydown 激活。
2221
+ confirm.addEventListener("keydown", (event) => event.stopPropagation());
2222
+ }
2223
+ // 仅完成提醒(done)卡显示确认按钮;阻塞等待卡隐藏(其解除是真正回答)。
2224
+ confirm.hidden = entry.waitClass !== "done";
2225
+ }
2226
+ }
2227
+
2228
+ const note = el.querySelector(".dap-note");
2229
+ if (note !== null) {
2230
+ const hasQuestionList =
2231
+ entry.kind === "awaiting" &&
2232
+ entry.waitClass === "blocked" &&
2233
+ entry.pendingKind === "question" &&
2234
+ renderQuestionPreview(note, entry.questionPreview);
2235
+ if (!hasQuestionList) {
2236
+ const next =
2237
+ entry.kind === "awaiting"
2238
+ ? (entry.noteText ?? "")
2239
+ : entry.kind === "recent"
2240
+ ? fmtRecentTime(entry.activityAt)
2241
+ : "";
2242
+ restoreTextField(note, next);
2243
+ }
2244
+ }
2245
+ }
2246
+
2247
+ // ---- 轮内状态订阅生命周期(R-01-009、R-02-004) ----
2248
+ function syncLiveness(runningIds, clockWanted) {
2249
+ for (const id of runningIds) {
2250
+ if (livenessById.has(id)) continue;
2251
+ let session = null;
2252
+ try {
2253
+ session = sessions?.binding?.(id)?.session ?? null;
2254
+ } catch {
2255
+ session = null;
2256
+ }
2257
+ if (session === null) continue; // 下次渲染再试
2258
+ let unsubscribe = null;
2259
+ try {
2260
+ unsubscribe = session.subscribe(() => {
2261
+ if (disposed) return;
2262
+ const snapshot = getSessionSnapshot(session);
2263
+ const live = livenessFromSnapshot(snapshot);
2264
+ livenessById.set(id, { unsubscribe, liveness: live, snapshot });
2265
+ const detail = sessionDetailsById.get(id) ?? {};
2266
+ detail.snapshot = snapshot;
2267
+ sessionDetailsById.set(id, detail);
2268
+ queueSync();
2269
+ });
2270
+ } catch {
2271
+ continue; // 订阅失败:本次跳过,下次渲染重试
2272
+ }
2273
+ try {
2274
+ opening = session.open?.();
2275
+ } catch {
2276
+ opening = null;
2277
+ }
2278
+ if (opening && typeof opening.then === "function") {
2279
+ const tracked = Promise.resolve(opening).then(
2280
+ () => {
2281
+ sessionOpenLoads.delete(id);
2282
+ queueSync();
2283
+ },
2284
+ () => {
2285
+ sessionOpenLoads.delete(id);
2286
+ queueSync();
2287
+ },
2288
+ );
2289
+ sessionOpenLoads.set(id, tracked);
2290
+ }
2291
+ const snapshot = getSessionSnapshot(session);
2292
+ livenessById.set(id, {
2293
+ unsubscribe,
2294
+ liveness: livenessFromSnapshot(snapshot),
2295
+ snapshot,
2296
+ });
2297
+ const detail = sessionDetailsById.get(id) ?? {};
2298
+ detail.snapshot = snapshot;
2299
+ sessionDetailsById.set(id, detail);
2300
+ }
2301
+ for (const [id, rec] of livenessById) {
2302
+ if (runningIds.has(id)) continue;
2303
+ try {
2304
+ rec.unsubscribe?.();
2305
+ } catch {}
2306
+ livenessById.delete(id);
2307
+ }
2308
+ // 运行卡时钟:有运行呈现卡(含委托周期母会话)才启动,无则停。
2309
+ if (clockWanted && clockTimer === null) {
2310
+ clockTimer = setInterval(() => queueSync(), CLOCK_MS);
2311
+ } else if (!clockWanted && clockTimer !== null) {
2312
+ clearInterval(clockTimer);
2313
+ clockTimer = null;
2314
+ }
2315
+ }
2316
+
2317
+ function getSessionSnapshot(session) {
2318
+ try {
2319
+ return session?.getSnapshot?.() ?? null;
2320
+ } catch {
2321
+ return null;
2322
+ }
2323
+ }
2324
+
2325
+ /** 保证 list 内存在/移除空态或加载指示节点(R-01-001/AC-02、R-01-014/AC-01)。
2326
+ * loading=true 时显示活动图标 + 文案(列表在途,禁止空态冒充)。 */
2327
+ function ensureListStatus(list, show, text, { loading = false } = {}) {
2328
+ let node = list.querySelector(".dap-empty");
2329
+ if (show) {
2330
+ if (node === null) {
2331
+ node = makeEl("div", "dap-empty");
2332
+ list.appendChild(node);
2333
+ }
2334
+ const mode = loading ? "loading" : "empty";
2335
+ if (node.dataset.mode !== mode) {
2336
+ node.dataset.mode = mode;
2337
+ if (loading) node.replaceChildren(makeEl("span", "dap-spinner"), document.createTextNode(text));
2338
+ else node.textContent = text;
2339
+ } else if (!loading && node.textContent !== text) {
2340
+ node.textContent = text;
2341
+ }
2342
+ } else if (node !== null) {
2343
+ node.remove();
2344
+ }
2345
+ }
2346
+
2347
+ /** 数量标识内容写入(R-01-014/AC-06):加载态显示活动指示——已是指示则不重写,
2348
+ * 避免每轮 replaceChildren 重启动画抖动;计数态恢复文本写入,textContent 赋值自动摘除指示。 */
2349
+ function setCountBadgeContent(el, badge) {
2350
+ if (badge.mode === "loading") {
2351
+ const spinner = el.firstElementChild;
2352
+ if (!(el.childNodes.length === 1 && spinner !== null && spinner.classList.contains("dap-spinner")))
2353
+ el.replaceChildren(makeEl("span", "dap-spinner"));
2354
+ } else if (el.textContent !== badge.text) {
2355
+ // 值未变不写文本节点:aria-live 下相同赋值也会触发替换与重复播报。
2356
+ el.textContent = badge.text;
2357
+ }
2358
+ }
2359
+
2360
+ /** 层级轨道层(R-01-003/AC-04):每个拥有可见直属子代理的母会话一条连续竖轨
2361
+ * 加每个子卡一条接入横线(拓扑与几何由 trackRuns/trackBoxes 纯函数给出),
2362
+ * 渲染提交后统一测量、取整到 CSS 像素后一次性写入。竖轨单元素整体绘制,
2363
+ * 不做分段拼接(接缝在随机亚像素相位下断口与重叠并存,T-033);横线同样
2364
+ * 由测量值驱动——任何由 CSS 按小数坐标定位的 1px 线段都会被抗锯齿随机
2365
+ * 摊薄,粗细不一、端点方头错位。测量集中在全部卡片写入之后,一次强制
2366
+ * 布局完成全部读取再统一写入,避免读写交替抖动。 */
2367
+ const trackEls = new Map();
2368
+ let trackedLayer = null;
2369
+ let observedTrackList = null;
2370
+ let trackContext = null;
2371
+ let layerShiftX = 0;
2372
+ let layerShiftY = 0;
2373
+ const trackResizeObserver = new ResizeObserver(() => {
2374
+ if (trackContext !== null) syncTracks(trackContext.list, trackContext.entries, trackContext.cardsMap);
2375
+ });
2376
+ // 滚动停在任意小数相位上时重对齐轨道层(rAF 合帧,键值未变时只写 transform)。
2377
+ let trackSyncHandle = null;
2378
+ function queueTrackSync() {
2379
+ if (trackSyncHandle !== null) return;
2380
+ trackSyncHandle = requestAnimationFrame(() => {
2381
+ trackSyncHandle = null;
2382
+ if (trackContext !== null) syncTracks(trackContext.list, trackContext.entries, trackContext.cardsMap);
2383
+ });
2384
+ }
2385
+
2386
+ function syncTracks(list, entries, cardsMap) {
2387
+ const layer = list.querySelector(".dap-tracks");
2388
+ if (layer === null) return;
2389
+ if (layer !== trackedLayer) {
2390
+ // 窗格重挂载后旧轨道元素已随旧 pane 移除,记账一并重置。
2391
+ trackEls.clear();
2392
+ trackedLayer = layer;
2393
+ layerShiftX = 0;
2394
+ layerShiftY = 0;
2395
+ }
2396
+ if (list !== observedTrackList) {
2397
+ if (observedTrackList !== null) trackResizeObserver.unobserve(observedTrackList);
2398
+ trackResizeObserver.observe(list);
2399
+ observedTrackList = list;
2400
+ }
2401
+ trackContext = { list, entries, cardsMap };
2402
+ // id 统一按字符串查(buildEntries 的 parentId 已 String 化,而卡片表按原始 id 键控)。
2403
+ const recById = new Map();
2404
+ for (const entry of entries) {
2405
+ const rec = cardsMap.get(entry.id);
2406
+ if (rec !== undefined) recById.set(String(entry.id), rec);
2407
+ }
2408
+ // 必须用浮点矩形:offsetTop/offsetHeight 是整数舍入值,而卡片横线由 CSS 按
2409
+ // 全精度小数高度定位(top:50%)——一边取整一边全精度,轨道端点会随机差
2410
+ // 1~2px(东家验收发现)。getBoundingClientRect 与 CSS 同精度,端点恒等。
2411
+ // 两个矩形同帧同参考系(相对轨道层未平移原点),滚动不影响差值。
2412
+ const layerRect = layer.getBoundingClientRect();
2413
+ const baseLeft = layerRect.left - layerShiftX;
2414
+ const baseTop = layerRect.top - layerShiftY;
2415
+ const rectOf = (id) => {
2416
+ const rec = recById.get(id);
2417
+ if (rec === undefined) return null;
2418
+ const rect = rec.el.getBoundingClientRect();
2419
+ return { top: rect.top - baseTop, height: rect.height, left: rect.left - baseLeft };
2420
+ };
2421
+ // 几何推导在纯函数 trackBoxes(可执行断言钉住);此处只做测量与 DOM 写入。
2422
+ const wanted = new Map();
2423
+ for (const run of trackRuns(entries)) {
2424
+ const boxes = trackBoxes(run, rectOf, INDENT_PX);
2425
+ if (boxes === null) continue; // 卡片缺失或折叠/隐藏态零读数,展开后由 ResizeObserver 重算
2426
+ wanted.set(run.parentId, boxes);
2427
+ }
2428
+ // 设备像素对齐:1px 线段只有落在设备像素边界上才粗细一致。层原点随窗口
2429
+ // 布局/滚动停在任意小数相位(东家验收:竖轨 2px、横线 1px),整体平移层
2430
+ // (≤0.5px,接头随层同步、不可见)使全部线段同相位清晰渲染。写入集中在
2431
+ // 全部测量之后,不产生读写交替。
2432
+ const dpr = window.devicePixelRatio || 1;
2433
+ const nextShiftX = Math.round(baseLeft * dpr) / dpr - baseLeft;
2434
+ const nextShiftY = Math.round(baseTop * dpr) / dpr - baseTop;
2435
+ if (nextShiftX !== layerShiftX || nextShiftY !== layerShiftY) {
2436
+ layerShiftX = nextShiftX;
2437
+ layerShiftY = nextShiftY;
2438
+ layer.style.transform = `translate(${nextShiftX}px, ${nextShiftY}px)`;
2439
+ }
2440
+ for (const [pid, rec] of trackEls) {
2441
+ if (wanted.has(pid)) continue;
2442
+ rec.trackEl.remove();
2443
+ for (const el of rec.stubEls) el.remove();
2444
+ trackEls.delete(pid);
2445
+ }
2446
+ for (const [pid, next] of wanted) {
2447
+ const key = JSON.stringify(next);
2448
+ let rec = trackEls.get(pid);
2449
+ if (rec === undefined) {
2450
+ const trackEl = document.createElement("i");
2451
+ trackEl.className = "dap-conn-track";
2452
+ layer.appendChild(trackEl);
2453
+ rec = { trackEl, stubEls: [], key: null };
2454
+ trackEls.set(pid, rec);
2455
+ }
2456
+ if (rec.key === key) continue;
2457
+ rec.key = key;
2458
+ rec.trackEl.style.top = `${next.track.top}px`;
2459
+ rec.trackEl.style.height = `${next.track.height}px`;
2460
+ rec.trackEl.style.left = `${next.track.left}px`;
2461
+ for (const el of rec.stubEls) el.remove();
2462
+ rec.stubEls = [];
2463
+ for (const stub of next.stubs) {
2464
+ const el = document.createElement("i");
2465
+ el.className = "dap-conn-stub";
2466
+ el.style.top = `${stub.top}px`;
2467
+ el.style.left = `${stub.left}px`;
2468
+ el.style.width = `${stub.width}px`;
2469
+ layer.appendChild(el);
2470
+ rec.stubEls.push(el);
2471
+ }
2472
+ }
2473
+ }
2474
+
2475
+ /** 渲染某一张卡片进指定列表容器(活动/历史通用)。index 是条目在卡片序列中的
2476
+ * 序号,offset 是容器内首个卡片前的非卡片子节点数(活动区有轨道层、历史区有段头)。 */
2477
+ function renderCardIntoList(list, entry, reuseMap, index, offset, hueByWorkspace) {
2478
+ let rec = reuseMap.get(entry.id);
2479
+ if (rec === undefined) {
2480
+ const el = document.createElement("div");
2481
+ el.className = CARD_CLASS;
2482
+ const unbind = bindCardActivation(el, (sessionId) => {
2483
+ if (typeof sessions?.open !== "function") return;
2484
+ lastActivatedId = sessionId;
2485
+ // 新激活意图取代一切旧重试链,避免过期链条稍后把当前会话拽回旧目标;
2486
+ // 收起抽屉的分支同样是最新意图,必须先取消挂起链条再 return。
2487
+ cancelStaleOpenRetries({ activatedId: sessionId });
2488
+ // 二次激活当前会话卡片:移动断点抽屉打开时收起抽屉直达会话
2489
+ //(R-01-008/AC-06),不发起会话切换。
2490
+ if (
2491
+ shouldDismissDrawerOnActivation({
2492
+ targetId: sessionId,
2493
+ currentId: getSnapshot(sessions, "list")?.current ?? null,
2494
+ mobile: window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT})`).matches,
2495
+ drawerOpen:
2496
+ document.querySelector(`[${PANE_ATTR}]`)?.getAttribute("data-open") === "true",
2497
+ })
2498
+ ) {
2499
+ togglePane(false);
2500
+ return;
2501
+ }
2502
+ attemptOpen(sessionId, 0);
2503
+ });
2504
+ rec = { el, kind: null, unbind };
2505
+ reuseMap.set(entry.id, rec);
2506
+ }
2507
+ if (rec.kind !== entry.kind) {
2508
+ rec.el.dataset.kind = entry.kind;
2509
+ rec.el.dataset.sessionId = entry.id;
2510
+ rec.el.dataset.depth = String(entry.depth ?? 0);
2511
+ rec.el.setAttribute("role", "button");
2512
+ rec.el.tabIndex = 0;
2513
+ rec.el.replaceChildren(...cardChildren(entry.kind));
2514
+ rec.kind = entry.kind;
2515
+ }
2516
+ rec.el.style.marginLeft = `${(entry.depth ?? 0) * INDENT_PX}px`;
2517
+ rec.el.toggleAttribute("data-current", entry.isCurrent);
2518
+ rec.el.toggleAttribute("data-awaiting", entry.kind === "awaiting");
2519
+ // 等待三类(R-01-002/AC-08、AC-13,C-043):blocked=阻塞等待(金色卡面、胶囊+正文闪烁),
2520
+ // done=完成提醒(绿色成功卡面、胶囊+正文闪烁、按钮不闪),error=错误提醒(红色卡面、
2521
+ // 胶囊+正文闪烁、无按钮)。
2522
+ if (entry.waitClass === "blocked" || entry.waitClass === "done" || entry.waitClass === "error")
2523
+ rec.el.setAttribute("data-wait", entry.waitClass);
2524
+ else rec.el.removeAttribute("data-wait");
2525
+ rec.el.setAttribute(
2526
+ "aria-label",
2527
+ `${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
2528
+ entry.pendingText ? "," + entry.pendingText : ""
2529
+ }${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}`,
2530
+ );
2531
+ renderCardInto(rec.el, entry, hueByWorkspace);
2532
+ // 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
2533
+ // 卡片瞬时移除再插回——按下/抬起之间经过的移动让浏览器取消 click;焦点卡
2534
+ // 被瞬时断开而失焦;悬停卡的 :hover 也随之丢失且不再补发。会话活跃期间
2535
+ // 渲染随推送/时钟高频发生,窗格因此整体「不响应」。
2536
+ const ref = list.children[offset + index] ?? null;
2537
+ if (ref !== rec.el) list.insertBefore(rec.el, ref);
2538
+ return true;
2539
+ }
2540
+
2541
+ function pruneCards(reuseMap, alive) {
2542
+ for (const [id, rec] of reuseMap) {
2543
+ if (alive.has(id)) continue;
2544
+ rec.unbind?.();
2545
+ cancelShift(rec.el);
2546
+ rec.el.remove();
2547
+ reuseMap.delete(id);
2548
+ }
2549
+ }
2550
+
2551
+ /** 降低动效偏好(R-01-010/AC-07):命中时迁移直接落位,不播放动画。 */
2552
+ function prefersReducedMotion() {
2553
+ try {
2554
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2555
+ } catch {
2556
+ return false;
2557
+ }
2558
+ }
2559
+
2560
+ function removeMoveGhost(id, ghost) {
2561
+ moveGhosts.delete(ghost);
2562
+ if (moveGhostsById.get(id) === ghost) moveGhostsById.delete(id);
2563
+ ghost.remove();
2564
+ }
2565
+
2566
+ /** 取消元素在飞平移:移除 transitionend 监听、过渡类与内联位移(瞬时不渐变)。
2567
+ * 平移中再次迁移时以当前视觉矩形为起点重启前调用;prune 与卸载时同步调用。 */
2568
+ function cancelShift(el) {
2569
+ const cleanup = shiftCleanups.get(el);
2570
+ if (cleanup === undefined) return;
2571
+ el.removeEventListener("transitionend", cleanup);
2572
+ el.classList.remove("dap-shift");
2573
+ el.style.transform = "";
2574
+ shiftCleanups.delete(el);
2575
+ }
2576
+
2577
+ /** 迁移动画前半(DOM 写入前):量取迁出卡矩形(换算为窗格相对坐标)并克隆
2578
+ * ghost;ghost 挂载于窗格元素内,卡片样式经作用域自然生效。migrations 为双向
2579
+ * 迁移计划 [{ id, from, to }],from/to 分别为源/目标卡池(R-01-010/AC-07)。 */
2580
+ function prepareMoveGhosts(migrations) {
2581
+ if (migrations.length === 0 || disposed || prefersReducedMotion()) return [];
2582
+ const paneRect = renderedPane?.getBoundingClientRect?.();
2583
+ if (paneRect == null) return [];
2584
+ const plans = [];
2585
+ for (const { id, from, to } of migrations) {
2586
+ const source = from.get(id)?.el;
2587
+ const rect = source?.getBoundingClientRect?.();
2588
+ if (source == null || rect == null || rect.width <= 0 || rect.height <= 0) continue;
2589
+ // 同一 id 再次迁移:先移除旧 ghost,按最新帧重新判定。
2590
+ const oldGhost = moveGhostsById.get(id);
2591
+ if (oldGhost !== undefined) removeMoveGhost(id, oldGhost);
2592
+ const ghost = source.cloneNode(true);
2593
+ ghost.classList.add("dap-move-ghost");
2594
+ ghost.removeAttribute("data-session-id");
2595
+ ghost.removeAttribute("id");
2596
+ ghost.style.marginLeft = "0";
2597
+ ghost.style.boxSizing = "border-box";
2598
+ ghost.style.left = `${rect.left - paneRect.left}px`;
2599
+ ghost.style.top = `${rect.top - paneRect.top}px`;
2600
+ ghost.style.width = `${rect.width}px`;
2601
+ ghost.style.height = `${rect.height}px`;
2602
+ // 克隆在此不挂载:其后 DOM 写入若中途抛错,未挂载克隆随 plans 自然丢弃,
2603
+ // 覆盖层不会无 transition 残留(transitionend 收口的唯一兜底)。
2604
+ plans.push({ id, to, ghost, left: rect.left - paneRect.left, top: rect.top - paneRect.top });
2605
+ }
2606
+ return plans;
2607
+ }
2608
+
2609
+ /** 迁移动画后半(DOM 写入后):量取目标区卡片矩形,rAF 内启动 FLIP 平移并形变至
2610
+ * 目标矩形,到位后淡出、真卡淡入;目标不可量取时直接落位(克隆未挂载,无需清理)。
2611
+ * transitionend 收口,不引入定时器。 */
2612
+ function runMoveGhosts(plans) {
2613
+ const paneRect = renderedPane?.getBoundingClientRect?.();
2614
+ if (paneRect == null) return;
2615
+ for (const plan of plans) {
2616
+ const target = plan.to.get(plan.id)?.el;
2617
+ const rect = target?.getBoundingClientRect?.();
2618
+ if (target == null || rect == null || rect.width <= 0 || rect.height <= 0) {
2619
+ continue;
2620
+ }
2621
+ renderedPane.appendChild(plan.ghost);
2622
+ moveGhosts.add(plan.ghost);
2623
+ moveGhostsById.set(plan.id, plan.ghost);
2624
+ target.classList.add("dap-move-in");
2625
+ const dx = rect.left - paneRect.left - plan.left;
2626
+ const dy = rect.top - paneRect.top - plan.top;
2627
+ const ghost = plan.ghost;
2628
+ requestAnimationFrame(() => {
2629
+ if (!moveGhosts.has(ghost)) return;
2630
+ ghost.style.transform = `translate(${dx}px, ${dy}px)`;
2631
+ ghost.style.width = `${rect.width}px`;
2632
+ ghost.style.height = `${rect.height}px`;
2633
+ ghost.style.opacity = "0";
2634
+ });
2635
+ ghost.addEventListener(
2636
+ "transitionend",
2637
+ () => {
2638
+ removeMoveGhost(plan.id, ghost);
2639
+ target.classList.remove("dap-move-in");
2640
+ },
2641
+ { once: true },
2642
+ );
2643
+ }
2644
+ }
2645
+
2646
+ /** 受影响卡片 FLIP 前半(DOM 写入前,R-01-010/AC-10):量取两卡池全部现存卡片与
2647
+ * 历史区段头的当前视觉矩形(含在飞平移的 transform 位移,重启时从视觉位置续接)。
2648
+ * 零尺寸(整段隐藏等)不量取:无从滑起的元素不做动画。 */
2649
+ function snapshotShiftRects() {
2650
+ const rects = new Map();
2651
+ for (const pool of [cardsById, recentCardsById]) {
2652
+ for (const rec of pool.values()) {
2653
+ const rect = rec.el.getBoundingClientRect();
2654
+ if (rect.width > 0 && rect.height > 0) rects.set(rec.el, rect);
2655
+ }
2656
+ }
2657
+ const head = renderedPane?.querySelector(".dap-recent-head") ?? null;
2658
+ if (head !== null) {
2659
+ const rect = head.getBoundingClientRect();
2660
+ if (rect.width > 0 && rect.height > 0) rects.set(head, rect);
2661
+ }
2662
+ return rects;
2663
+ }
2664
+
2665
+ /** 受影响卡片 FLIP 后半(DOM 写入后,R-01-010/AC-10):仍连接且位置变化的元素先
2666
+ * 以无过渡内联 transform 钉回旧视觉位置,reflow 后挂 dap-shift 并清除内联位移,
2667
+ * 经 transform 过渡平滑归位;transitionend(once)收口。目标零尺寸(整段转隐藏)
2668
+ * 或位置未变时跳过。迁移卡的新元素不在量取集合内(由 ghost + 淡入承担呈现)。 */
2669
+ function runShiftAnimations(preRects) {
2670
+ for (const [el, pre] of preRects) {
2671
+ if (!el.isConnected) continue;
2672
+ // 平移中再次迁移:先取消旧平移(含监听,元素瞬回布局位),再量取新布局矩形——
2673
+ // 否则 post 会混入在飞 transform 位移,反向位移量算错。
2674
+ cancelShift(el);
2675
+ const post = el.getBoundingClientRect();
2676
+ if (post.width <= 0 || post.height <= 0) continue;
2677
+ const dx = pre.left - post.left;
2678
+ const dy = pre.top - post.top;
2679
+ if (dx === 0 && dy === 0) continue;
2680
+ // 以刚量取的旧视觉矩形为起点重启:先无过渡钉回,reflow 后挂过渡类归零。
2681
+ el.style.transform = `translate(${dx}px, ${dy}px)`;
2682
+ void el.offsetWidth;
2683
+ el.classList.add("dap-shift");
2684
+ el.style.transform = "";
2685
+ // transitionend 冒泡隔离:子元素过渡(如运行卡进度条 .dap-fill 的 width
2686
+ // 0.45s transition)的 transitionend 会冒泡到卡片,不过滤则平移中途被
2687
+ // 提前收口、卡片瞬时跳到终点(违背 R-01-010/AC-10);只收口本元素
2688
+ // transform 过渡,命中后手动移除监听(once 会被冒泡事件空耗,不可用)。
2689
+ const cleanup = (event) => {
2690
+ if (event.target !== el || event.propertyName !== "transform") return;
2691
+ el.removeEventListener("transitionend", cleanup);
2692
+ el.classList.remove("dap-shift");
2693
+ shiftCleanups.delete(el);
2694
+ };
2695
+ shiftCleanups.set(el, cleanup);
2696
+ el.addEventListener("transitionend", cleanup);
2697
+ }
2698
+ }
2699
+
2700
+ /** 卡片渲染异常上报(按会话+错误内容去重):持续故障不刷屏,错误变化时再记。
2701
+ * 隔离是为让其余卡片继续更新,错误本身必须保持可见、不吞错。 */
2702
+ let lastCardRenderErrorKey = "";
2703
+ function logCardRenderError(sessionId, error) {
2704
+ const key = `${sessionId}:${error instanceof Error ? error.message : String(error)}`;
2705
+ if (key === lastCardRenderErrorKey) return;
2706
+ lastCardRenderErrorKey = key;
2707
+ console.error(`[dsh-activity-pane] 卡片渲染失败(${sessionId}):`, error);
2708
+ }
2709
+
2710
+ function render() {
2711
+ installFrameObserver();
2712
+ const pane = ensurePane();
2713
+ if (pane === null) return;
2714
+ if (pane !== renderedPane) {
2715
+ renderedPane = pane;
2716
+ lastSig = "";
2717
+ pulseSignature = "";
2718
+ prevRenderedActiveIds = new Set();
2719
+ prevRenderedRecentIds = new Set();
2720
+ // 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
2721
+ for (const el of [...shiftCleanups.keys()]) cancelShift(el);
2722
+ }
2723
+ applyLayout();
2724
+ const activeList = pane.querySelector(`.${LIST_CLASS}`);
2725
+ const recentSection = pane.querySelector(`.${RECENT_CLASS}`);
2726
+ if (activeList === null || recentSection === null) return;
2727
+
2728
+ let snapshot = getSnapshot(sessions, "list");
2729
+ if (e2eListReadyAt > Date.now() && snapshot?.phase !== "error") {
2730
+ snapshot = { phase: "pending" };
2731
+ if (e2eListReleaseTimer === null) {
2732
+ e2eListReleaseTimer = setTimeout(() => {
2733
+ e2eListReleaseTimer = null;
2734
+ queueSync();
2735
+ }, Math.max(0, e2eListReadyAt - Date.now()));
2736
+ }
2737
+ }
2738
+ const listState = listLoadState(snapshot);
2739
+ const workspaceSnapshot = getSnapshot(workspaces, "list");
2740
+ const workspaceItems = workspaceSnapshot?.items ?? [];
2741
+ const archivedSessionIds = workspaceSnapshot?.archivedSessionIds ?? [];
2742
+ const now = Date.now();
2743
+
2744
+ // 委托周期集合(R-01-003/AC-05):由上一帧 progressAnchor 记账派生——后代耗尽
2745
+ // 至 settle 处理回合启动的空窗内,母会话保持运行呈现且不入历史区(分区不变量)。
2746
+ const delegatingIds = new Set();
2747
+ for (const [id, state] of progressAnchorById) {
2748
+ if (delegationActive(state, now)) delegatingIds.add(id);
2749
+ }
2750
+ const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds);
2751
+ // 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
2752
+ // 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
2753
+ const runLikeIds = new Set(
2754
+ active.filter((entry) => shouldSubscribeToSession(entry, snapshot?.byId ?? {})).map((entry) => entry.id),
2755
+ );
2756
+ syncLiveness(runLikeIds, active.some((entry) => entry.kind === "running"));
2757
+ for (const entry of active) {
2758
+ const liveRecord = livenessById.get(entry.id);
2759
+ const live = liveRecord?.liveness ?? null;
2760
+ const detail = sessionDetailsById.get(entry.id);
2761
+ const detailSnapshot = liveRecord?.snapshot ?? detail?.snapshot ?? null;
2762
+ if (detail && detail.memoHistoryAnchorOf !== (detail.history ?? null)) {
2763
+ detail.memoHistoryAnchorOf = detail.history ?? null;
2764
+ detail.memoHistoryAnchor = historyInstructionAnchor(detail.history);
2765
+ }
2766
+ if (detail && detailSnapshot) {
2767
+ // 按快照引用 memo:引用不变(时钟 tick、无关推送)时命中缓存,
2768
+ // 长会话不再每次渲染全序扫描。
2769
+ const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
2770
+ // 等待/暂停呈现(pendingText 存在)且自身快照为冻结值时,残留 running 行全部落定;
2771
+ // 存在活动后代时保留尾部提升的「agent 工作中」呈现(R-01-009/AC-10 委托周期语义)。
2772
+ const entryIdle = (entry.pendingText ?? null) !== null && entry.descendantActive !== true;
2773
+ if (detail.memoTimelineOf !== detailSnapshot || detail.memoTimelineCwd !== entryCwd || detail.memoTimelineDescendantActive !== (entry.descendantActive === true) || detail.memoTimelineIdle !== entryIdle || detail.memoTimelineAnchor !== (detail.memoHistoryAnchor ?? null)) {
2774
+ detail.memoTimelineOf = detailSnapshot;
2775
+ detail.memoTimelineCwd = entryCwd;
2776
+ detail.memoTimelineDescendantActive = entry.descendantActive === true;
2777
+ detail.memoTimelineIdle = entryIdle;
2778
+ detail.memoTimelineAnchor = detail.memoHistoryAnchor ?? null;
2779
+ detail.memoTimeline = foldedConversationTimeline(detailSnapshot, 4, entryCwd, entry.descendantActive === true, entryIdle, detail.memoTimelineAnchor);
2780
+ // 冷窗口兜底触发记账(R-01-012/AC-12):窗口内有可锚用户行时锚行机制保证其
2781
+ // 出现在输出(窗口行或锚行),反之需 history 补读。
2782
+ detail.snapshotHasAnchorableUserRow = detail.memoTimeline.some(isAnchorableUserRow);
2783
+ }
2784
+ entry.timeline = detail.memoTimeline.length > 0 ? detail.memoTimeline : detail.timeline ?? [];
2785
+ } else {
2786
+ entry.timeline = detail?.timeline ?? entry.timeline ?? [];
2787
+ }
2788
+ if (detail) {
2789
+ // history 开放回合起点兜底(R-01-009/AC-06)。
2790
+ if (detail.memoOpenTurnStartHistoryOf !== (detail.history ?? null) || detail.memoOpenTurnStartSnapOf !== detailSnapshot) {
2791
+ detail.memoOpenTurnStartHistoryOf = detail.history ?? null;
2792
+ detail.memoOpenTurnStartSnapOf = detailSnapshot;
2793
+ // minTurn 取快照已知最晚回合号(partial/turnTimings/turnEnds):history 拉取
2794
+ // 后若已切换新回合,旧开放回合起点判为陈旧不采用。
2795
+ const partialTurn = detailSnapshot?.partial?.turn;
2796
+ const hint = Math.max(
2797
+ 0,
2798
+ Number.isFinite(partialTurn) ? partialTurn : 0,
2799
+ ...mapKeys(detailSnapshot?.turnTimings),
2800
+ ...mapKeys(detailSnapshot?.turnEnds),
2801
+ );
2802
+ detail.memoOpenTurnStart = openTurnStartFromEvents(detail.history, hint);
2803
+ }
2804
+ }
2805
+ if (detail?.model) {
2806
+ entry.model = detail.model.model;
2807
+ entry.reasoning = detail.model.reasoning;
2808
+ }
2809
+ // 待回复卡列表补全(C-040、C-064):buildEntries 运行时快照时间线可能仍为空,
2810
+ // 而上面的 memo 才在本帧算出结构化提问预览;等待卡静止后常无下一帧,因此在此
2811
+ // 立即补入 questionPreview,由 cardSignature 驱动本帧 DOM 写入。
2812
+ if (entry.kind === "awaiting" && entry.waitClass === "blocked" && entry.pendingKind === "question") {
2813
+ const question = timelineQuestionPreview(entry.timeline);
2814
+ if (question !== null) entry.questionPreview = question;
2815
+ }
2816
+ // 字段级加载指示(R-01-014/AC-02):补充数据在途时卡片对应位置显示活动图标。
2817
+ entry.loadingModel = !detail?.model && modelLoads.has(entry.id);
2818
+ entry.loadingTimeline =
2819
+ entry.timeline.length === 0 &&
2820
+ (historyLoads.has(entry.id) || sessionOpenLoads.has(entry.id) || (runLikeIds.has(entry.id) && !liveRecord));
2821
+ // token/速率取自 sessions.list 条目的投影(tokenUsage/sessionStats),
2822
+ // 复用既有列表订阅,无新增轮询(R-01-009/AC-05、R-02-004/AC-02)。
2823
+ const projection = snapshot?.byId?.[entry.id]?.projectionValues;
2824
+ const stats = projection?.sessionStats;
2825
+ const rateTokS =
2826
+ stats && Number.isFinite(stats.decodeMs) && stats.decodeMs > 0
2827
+ ? stats.decodeTokens / (stats.decodeMs / 1000)
2828
+ : null;
2829
+ // 委托周期锚点(R-01-009/AC-06):全部活动条目逐帧记账——锚点不因呈现翻转
2830
+ // (委托期与 awaiting 互转)或瞬时不可见而丢失,仅 dispose 时整体清除;周期内
2831
+ // 进度连续(含 settle 处理回合),周期外由宿主回合起点驱动(新回合归零)。
2832
+ // 半衰期不记入锚点(C-044):每帧按最新实测累计速率现算,进度作为对完成度
2833
+ // 的实时估计允许随速率回落而回退。
2834
+ // 冷窗口兜底:快照 turnTimings 无开放回合起点(超长回合 turn/start 在尾页窗口
2835
+ // 之外)时,取 history 深翻提取的开放回合起点。
2836
+ const anchor = progressAnchor(progressAnchorById.get(entry.id) ?? null, {
2837
+ descendantActive: entry.descendantActive === true,
2838
+ hostStartTime: live?.startTime ?? detail?.memoOpenTurnStart ?? null,
2839
+ now,
2840
+ });
2841
+ progressAnchorById.set(entry.id, anchor);
2842
+ if (entry.kind === "running") {
2843
+ const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
2844
+ const outputTokens = projection?.tokenUsage?.outputTokens ?? null;
2845
+ Object.assign(entry, runtimeStats({ elapsedMs, outputTokens, rateTokS }));
2846
+ Object.assign(entry, usageSummary(projection?.tokenUsage ?? {}));
2847
+ // 回合进度:纯时间驱动 y = t/(t+k),半衰期每帧按最新实测速率现算,固定
2848
+ // k 下单调性由函数本身保证;k 变化时允许进度随速率回落而回退(委托周期
2849
+ // 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
2850
+ entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS }) });
2851
+ }
2852
+ }
2853
+ // 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
2854
+ // history 同批事件提取最后回合结束时刻(取两者较新者),按引用 memo;均无则不提供,
2855
+ // 由 buildRecent 回退宿主列表时间。history/快照到达后经 queueSync 重绘就地精化。
2856
+ const turnEnds = {};
2857
+ for (const [id, detail] of sessionDetailsById) {
2858
+ const detailSnapshot = livenessById.get(id)?.snapshot ?? detail.snapshot ?? null;
2859
+ if (detail.memoTurnEndSnapshotOf !== detailSnapshot || detail.memoTurnEndHistoryOf !== (detail.history ?? null)) {
2860
+ detail.memoTurnEndSnapshotOf = detailSnapshot;
2861
+ detail.memoTurnEndHistoryOf = detail.history ?? null;
2862
+ const ends = [lastTurnEndFromTimings(detailSnapshot?.turnTimings), lastTurnEndFromEvents(detail.history)].filter((v) => v !== null);
2863
+ detail.memoTurnEnd = ends.length > 0 ? Math.max(...ends) : null;
2864
+ }
2865
+ if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
2866
+ }
2867
+ const recent = buildRecent(snapshot, workspaceItems, now, undefined, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
2868
+ // 预览只对 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
2869
+ // 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
2870
+ const previewFallbackIds = new Set();
2871
+ for (const entry of recent) {
2872
+ const detail = sessionDetailsById.get(entry.id);
2873
+ if (!detail) continue;
2874
+ const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail.snapshot ?? null;
2875
+ const previewsKey = detailSnapshot ?? detail.history ?? null;
2876
+ if (detail.memoPreviewsOf !== previewsKey || detail.memoPreviewsHistoryOf !== (detail.history ?? null) || !detail.memoPreviews) {
2877
+ detail.memoPreviewsOf = previewsKey;
2878
+ detail.memoPreviewsHistoryOf = detail.history ?? null;
2879
+ detail.memoPreviews = messagePreviews({ snapshot: detailSnapshot, history: detail.history });
2880
+ }
2881
+ entry.userPreview = detail.memoPreviews.userPreview || detail.previews?.userPreview || "";
2882
+ entry.agentPreview = detail.memoPreviews.agentPreview || detail.previews?.agentPreview || "";
2883
+ if (!entry.userPreview || !entry.agentPreview) previewFallbackIds.add(entry.id);
2884
+ entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
2885
+ }
2886
+ // 补充数据读取优先级:当前会话最优先,活动区先于历史区(区内按显示顺序)。
2887
+ const detailIds = [...active, ...recent].map((entry) => entry.id);
2888
+ detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
2889
+ loadNativeDetails(detailIds, previewFallbackIds);
2890
+ const visibleIds = new Set([...active, ...recent].map((entry) => entry.id));
2891
+ // 详情与 loads 记账同生命周期:离开可见集合即放行,重回可见时允许重拉/重试。
2892
+ // 锚点记账不随可见性 prune:瞬时 loading 空帧不得误清(进度重置);陈旧条目靠
2893
+ // 耗尽宽限与 turnStart 不匹配自校正,仅在 dispose 时整体清除。
2894
+ pruneInvisibleEntries([sessionDetailsById, modelLoads, historyLoads, sessionOpenLoads], visibleIds);
2895
+ // 模型目录订阅同生命周期:不可见即先 unsubscribe 再除名,监听器不残留(R-01-012/AC-16)。
2896
+ pruneSubscriptions(modelDirectorySubs, visibleIds);
2897
+ // 重试链目标已成为当前会话(他途到达)即取消,避免过期链条拽回会话。
2898
+ cancelStaleOpenRetries({ currentId: snapshot?.current ?? null, activatedId: lastActivatedId });
2899
+
2900
+ const visibleEntries = [...active, ...recent];
2901
+ const pulseSurface = desktopQuery.matches
2902
+ ? collapsed ? "rail" : "header"
2903
+ : pane.getAttribute("data-open") === "true" ? "drawer" : "toggle";
2904
+ // listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
2905
+ // 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
2906
+ // 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
2907
+ const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface]);
2908
+ if (sig === lastSig) return;
2909
+ const hueByWorkspace = resolveWorkspaceHues(visibleEntries.map((entry) => entry.workspaceKey));
2910
+ // 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
2911
+ const migrations = [
2912
+ ...movedToRecentIds(prevRenderedActiveIds, active, recent).map((id) => ({ id, from: cardsById, to: recentCardsById })),
2913
+ ...movedToActiveIds(prevRenderedRecentIds, active, recent).map((id) => ({ id, from: recentCardsById, to: cardsById })),
2914
+ ];
2915
+ const movePlans = prepareMoveGhosts(migrations);
2916
+ // 迁移帧内位置受影响的其它卡片与历史区段头:DOM 写入前量取当前视觉矩形
2917
+ // (R-01-010/AC-10);reduced-motion 下 movePlans 为空,FLIP 量取整体跳过。
2918
+ const shiftRects = movePlans.length > 0 ? snapshotShiftRects() : null;
2919
+
2920
+ // 逐卡异常隔离:一张卡渲染抛错不得冻结其余卡片(此前签名先于循环提交,
2921
+ // 故障卡及其后全部卡片永久滞留旧内容——历史卡因此停在过去的首条消息
2922
+ // fallback 标题,与左侧栏脱节);本轮有失败则不提交签名,下一次同步整体
2923
+ // 重试——瞬时异常自愈,持续异常只滞留故障卡本身(R-01-013/AC-02)。
2924
+ let renderOk = true;
2925
+ const aliveActive = new Set();
2926
+ for (const [index, entry] of active.entries()) {
2927
+ try {
2928
+ renderCardIntoList(activeList, entry, cardsById, index, 1, hueByWorkspace);
2929
+ } catch (error) {
2930
+ renderOk = false;
2931
+ logCardRenderError(entry.id, error);
2932
+ }
2933
+ // 渲染失败的卡同样计为存活:保留最后内容在位(下轮重试自愈),
2934
+ // 不得被 pruneCards 摘除后每轮重建闪烁。
2935
+ aliveActive.add(entry.id);
2936
+ }
2937
+ pruneCards(cardsById, aliveActive);
2938
+ // 全部卡片写入后统一测量绘制母会话轨道(单元素连续轨道,零拼接接缝)。
2939
+ syncTracks(activeList, active, cardsById);
2940
+ ensureListStatus(activeList, active.length === 0, listState === "loading" ? "加载中…" : listState === "error" ? "列表加载失败" : "暂无活动会话", { loading: listState === "loading" });
2941
+ const aliveRecent = new Set();
2942
+ // 历史区容器首个子节点是段头(.dap-recent-head),卡片从 offset 1 开始。
2943
+ for (const [index, entry] of recent.entries()) {
2944
+ try {
2945
+ renderCardIntoList(recentSection, entry, recentCardsById, index, 1, hueByWorkspace);
2946
+ } catch (error) {
2947
+ renderOk = false;
2948
+ logCardRenderError(entry.id, error);
2949
+ }
2950
+ aliveRecent.add(entry.id);
2951
+ }
2952
+ pruneCards(recentCardsById, aliveRecent);
2953
+ if (recentSection !== null) {
2954
+ recentSection.hidden = listState === "ready" && recent.length === 0;
2955
+ ensureListStatus(recentSection, listState !== "ready" && recent.length === 0, listState === "loading" ? "加载中…" : "列表加载失败", { loading: listState === "loading" });
2956
+ }
2957
+ runMoveGhosts(movePlans);
2958
+ if (shiftRects !== null) runShiftAnimations(shiftRects);
2959
+ // 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
2960
+ const headerEl = pane.querySelector(".dap-header");
2961
+ const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
2962
+ for (const [head, hasItems] of [[headerEl, active.length > 0], [recentHeadEl, recent.length > 0]]) {
2963
+ if (head === null) continue;
2964
+ const spin = head.querySelector(".dap-spinner");
2965
+ if (listState === "loading" && hasItems) {
2966
+ if (spin === null) head.prepend(makeEl("span", "dap-spinner"));
2967
+ } else {
2968
+ spin?.remove();
2969
+ }
2970
+ }
2971
+
2972
+ // 计数与折叠:n/m 只统计主会话——分子为等待行动数、分母为其加运行中主会话之和
2973
+ // (R-01-001/AC-04、AC-05);空态同样显示 0/0(AC-06)。列表在途时不冒充计数,
2974
+ // 三处数量标识显示加载指示(R-01-014/AC-06)。脉冲由 data-awaiting 承载:
2975
+ // 任一等待行动(阻塞等待、完成提醒或错误提醒)即脉冲(R-01-002/AC-06,C-037);
2976
+ // 底色经 data-tone 跟随等待构成——错误 > 阻塞 > 完成 优先级取红/金/绿(C-040、C-043)。
2977
+ const count = pane.querySelector(".dap-count");
2978
+ const railCount = pane.querySelector(".dap-rail-count");
2979
+ const { waiting, blocked, total } = awaitBadgeStats(active);
2980
+ const badge = countBadgeState(listState, waiting, total, blocked);
2981
+ const badgeTone = awaitBadgeTone(active);
2982
+ for (const el of [count, railCount])
2983
+ if (el !== null) {
2984
+ setCountBadgeContent(el, badge);
2985
+ el.toggleAttribute("data-awaiting", badge.awaiting);
2986
+ if (badge.awaiting && badgeTone !== null) el.setAttribute("data-tone", badgeTone);
2987
+ else el.removeAttribute("data-tone");
2988
+ if (el.getAttribute("aria-label") !== badge.ariaText) el.setAttribute("aria-label", badge.ariaText);
2989
+ }
2990
+ const toggleCount = toggle.querySelector(".dap-toggle-count");
2991
+ if (toggleCount !== null) {
2992
+ setCountBadgeContent(toggleCount, badge);
2993
+ toggle.toggleAttribute("data-awaiting", badge.awaiting);
2994
+ if (badge.awaiting && badgeTone !== null) toggle.setAttribute("data-tone", badgeTone);
2995
+ else toggle.removeAttribute("data-tone");
2996
+ }
2997
+ const nextPulseSignature = JSON.stringify([
2998
+ pulseSurface,
2999
+ active
3000
+ .filter((entry) => entry.kind === "awaiting")
3001
+ .map((entry) => `${entry.id}:${entry.waitClass ?? ""}`)
3002
+ .sort(),
3003
+ ]);
3004
+ if (pulseSignature !== nextPulseSignature) {
3005
+ syncAwaitPulse([
3006
+ count,
3007
+ railCount,
3008
+ toggleCount,
3009
+ ...pane.querySelectorAll('.dap-card[data-kind="awaiting"] .dap-foot :is(.dap-capsule, .dap-note)'),
3010
+ ]);
3011
+ }
3012
+ pane.toggleAttribute("data-collapsed", collapsed);
3013
+ const headerExpanded = collapsed ? "false" : "true";
3014
+ if (headerEl !== null && headerEl.getAttribute("aria-expanded") !== headerExpanded)
3015
+ headerEl.setAttribute("aria-expanded", headerExpanded);
3016
+
3017
+ // 渲染签名在整轮 DOM 写入全部成功后提交:任何一步失败都保留下一轮
3018
+ // 同步重试的机会,避免故障被签名吞掉后卡片永久滞留(R-01-013/AC-02)。
3019
+ if (renderOk) {
3020
+ lastSig = sig;
3021
+ pulseSignature = nextPulseSignature;
3022
+ prevRenderedActiveIds = new Set(active.map((entry) => String(entry.id)));
3023
+ prevRenderedRecentIds = new Set(recent.map((entry) => String(entry.id)));
3024
+ }
3025
+ }
3026
+
3027
+ // ---- 打开会话(让 sessions.open 自己校验列表,失败时 refresh + 重试) ----
3028
+ const MAX_OPEN_ATTEMPTS = 60;
3029
+ function cardElFor(sessionId) {
3030
+ return document.querySelector(`[${PANE_ATTR}] [data-session-id="${escapeCssString(sessionId)}"]`);
3031
+ }
3032
+ function cancelOpenRetry(sessionId) {
3033
+ const state = openRetryStates.get(sessionId);
3034
+ if (state !== undefined) {
3035
+ state.cancelled = true;
3036
+ if (state.timer !== null) clearTimeout(state.timer);
3037
+ openRetryStates.delete(sessionId);
3038
+ }
3039
+ cardElFor(sessionId)?.removeAttribute("data-opening");
3040
+ }
3041
+ /** 按最新意图批量取消过期重试链:目标已到达(currentId 命中)或已被新激活取代。 */
3042
+ function cancelStaleOpenRetries({ currentId = null, activatedId = null } = {}) {
3043
+ for (const id of [...openRetryStates.keys()])
3044
+ if (shouldCancelOpenRetry({ targetId: id, currentId, activatedId })) cancelOpenRetry(id);
3045
+ }
3046
+ function scheduleOpenRetry(sessionId, attempt) {
3047
+ if (disposed || openRetryStates.has(sessionId)) return;
3048
+ const state = { cancelled: false, timer: null };
3049
+ openRetryStates.set(sessionId, state);
3050
+ let refreshed;
3051
+ try {
3052
+ refreshed = sessions?.refresh?.();
3053
+ } catch {}
3054
+ Promise.resolve(refreshed)
3055
+ .catch(() => {})
3056
+ .finally(() => {
3057
+ if (
3058
+ disposed ||
3059
+ state.cancelled ||
3060
+ openRetryStates.get(sessionId) !== state
3061
+ )
3062
+ return;
3063
+ state.timer = setTimeout(() => {
3064
+ if (disposed || state.cancelled) return;
3065
+ openRetryStates.delete(sessionId);
3066
+ attemptOpen(sessionId, attempt);
3067
+ }, 500);
3068
+ });
3069
+ }
3070
+ function attemptOpen(sessionId, attempt) {
3071
+ // 过期链条(目标已到达 / 已被新激活意图取代)不再发起 open,防止拽回会话。
3072
+ if (
3073
+ shouldCancelOpenRetry({
3074
+ targetId: sessionId,
3075
+ currentId: getSnapshot(sessions, "list")?.current ?? null,
3076
+ activatedId: lastActivatedId,
3077
+ })
3078
+ ) {
3079
+ cancelOpenRetry(sessionId);
3080
+ return;
3081
+ }
3082
+ const el = cardElFor(sessionId);
3083
+ if (el !== null) el.setAttribute("data-opening", "");
3084
+
3085
+ if (openSession(sessions, sessionId)) {
3086
+ // 移动视口下抑制原生 composer 在 sessionId 变化后的自动聚焦,
3087
+ // 避免切换会话弹出软键盘(桌面保持原生自动聚焦)。
3088
+ if (!desktopQuery.matches) suppressComposerAutofocus(document);
3089
+ // 到达新目标后取消全部剩余链条:任何旧链成功都会把会话从新目标拽走。
3090
+ cancelStaleOpenRetries({ activatedId: sessionId });
3091
+ cancelOpenRetry(sessionId);
3092
+ el?.removeAttribute("data-opening");
3093
+ return;
3094
+ }
3095
+ if (attempt < MAX_OPEN_ATTEMPTS) scheduleOpenRetry(sessionId, attempt + 1);
3096
+ else cancelOpenRetry(sessionId);
3097
+ }
3098
+ // ---- 观察者:只监听宿主结构与 conversation seat 的流式 DOM ----
3099
+ let bodyObserver = null;
3100
+ /** 祖先链观察者:center → body 逐级一个,任一级断裂即重装。 */
3101
+ let ancestorObservers = [];
3102
+ function disconnectAncestorObservers() {
3103
+ for (const observer of ancestorObservers) observer.disconnect();
3104
+ ancestorObservers = [];
3105
+ }
3106
+ let centerObserver = null;
3107
+ let conversationObserver = null;
3108
+ let observedCenter = null;
3109
+ let observedSeat = null;
3110
+
3111
+ function installFrameObserver() {
3112
+ const seat = document.querySelector(CONVERSATION_SELECTOR);
3113
+ const center = seat?.parentElement ?? null;
3114
+ if (center === null) {
3115
+ disconnectAncestorObservers();
3116
+ centerObserver?.disconnect();
3117
+ conversationObserver?.disconnect();
3118
+ centerObserver = null;
3119
+ conversationObserver = null;
3120
+ observedCenter = null;
3121
+ observedSeat = null;
3122
+ bodyObserver?.observe(document.body, { childList: true, subtree: true });
3123
+ return false;
3124
+ }
3125
+ if (center === observedCenter && seat === observedSeat) return true;
3126
+
3127
+ disconnectAncestorObservers();
3128
+ centerObserver?.disconnect();
3129
+ conversationObserver?.disconnect();
3130
+
3131
+ if (center.parentElement === null) {
3132
+ bodyObserver?.observe(document.body, { childList: true, subtree: true });
3133
+ return false;
3134
+ }
3135
+ // 观察 center → body 的整条祖先链:外壳替换任一级祖先(含高于 parent 的
3136
+ // 视图级重挂载)都会命中对应观察者,断裂即重装 + 重绘(GF2 自愈)。
3137
+ for (let ancestor = center.parentElement; ancestor !== null; ancestor = ancestor.parentElement) {
3138
+ const observer = new MutationObserver(() => {
3139
+ if (
3140
+ !center.isConnected ||
3141
+ document.querySelector(CONVERSATION_SELECTOR)?.parentElement !== center
3142
+ ) {
3143
+ installFrameObserver();
3144
+ queueSync();
3145
+ }
3146
+ });
3147
+ observer.observe(ancestor, { childList: true });
3148
+ ancestorObservers.push(observer);
3149
+ }
3150
+
3151
+ // 只观察 center 的直接子节点,捕获 seat/pane 重挂载,不观察 pane 子树。
3152
+ centerObserver = new MutationObserver(() => {
3153
+ const nextSeat = document.querySelector(CONVERSATION_SELECTOR);
3154
+ if (nextSeat?.parentElement !== center) installFrameObserver();
3155
+ queueSync();
3156
+ });
3157
+ centerObserver.observe(center, { childList: true });
3158
+
3159
+ // 流式更新只来自会话 seat;pane 自身的文字/属性写入不会再触发重绘。
3160
+ conversationObserver = new MutationObserver(queueSync);
3161
+ conversationObserver.observe(seat, { childList: true, subtree: true });
3162
+ observedCenter = center;
3163
+ observedSeat = seat;
3164
+ bodyObserver?.disconnect();
3165
+ return true;
3166
+ }
3167
+
3168
+ // 槽座迟到时用 DOM 通知唤醒一次;找到宿主后立即断开 body 全树观察。
3169
+ bodyObserver = new MutationObserver(() => {
3170
+ if (installFrameObserver()) queueSync();
3171
+ });
3172
+ bodyObserver.observe(document.body, { childList: true, subtree: true });
3173
+ installFrameObserver();
3174
+
3175
+ // ---- 交互:移动端抽屉开关、弹窗收起、桌面折叠 ----
3176
+ function onToggleClick() {
3177
+ const pane = document.querySelector(`[${PANE_ATTR}]`);
3178
+ const open = pane?.getAttribute("data-open") !== "true";
3179
+ togglePane(open);
3180
+ }
3181
+ toggle.addEventListener("click", onToggleClick);
3182
+ // 透明遮罩位于抽屉与开关之下,能到达遮罩的点击均为抽屉外部,点击即收起。
3183
+ const unbindBackdrop = bindBackdropDismiss(backdrop, () => togglePane(false));
3184
+ const onResize = () => queueSync();
3185
+ desktopQuery.addEventListener("change", onResize);
3186
+
3187
+ installServiceSubscriptions();
3188
+ queueSync();
3189
+
3190
+ const cleanup = () => {
3191
+ disposed = true;
3192
+ sessionUnsubscribe?.();
3193
+ workspaceUnsubscribe?.();
3194
+ acksSource?.close();
3195
+ acksSource = null;
3196
+ document.removeEventListener("visibilitychange", onVisibilityResume);
3197
+ window.removeEventListener("pageshow", onPageShow);
3198
+ completeAcksById.clear();
3199
+ if (clockTimer !== null) clearInterval(clockTimer);
3200
+ if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
3201
+ for (const [timer, resolve] of e2eModelDelayWaiters) {
3202
+ clearTimeout(timer);
3203
+ resolve(null);
3204
+ }
3205
+ e2eModelDelayWaiters.clear();
3206
+ for (const [, rec] of livenessById) {
3207
+ try {
3208
+ rec.unsubscribe?.();
3209
+ } catch {}
3210
+ }
3211
+ livenessById.clear();
3212
+ // 卸载即全量退订:空可见集合驱动 pruneSubscriptions 先 unsubscribe 再除名。
3213
+ pruneSubscriptions(modelDirectorySubs, new Set());
3214
+ progressAnchorById.clear();
3215
+ sessionOpenLoads.clear();
3216
+ loadQueue.length = 0;
3217
+ loadInflight = 0;
3218
+ for (const rec of cardsById.values()) rec.unbind?.();
3219
+ for (const rec of recentCardsById.values()) rec.unbind?.();
3220
+ cardsById.clear();
3221
+ recentCardsById.clear();
3222
+ for (const sessionId of openRetryStates.keys()) cancelOpenRetry(sessionId);
3223
+ openRetryStates.clear();
3224
+ unbindPaneControls?.();
3225
+ unbindPaneControls = null;
3226
+ boundPane = null;
3227
+ renderedPane = null;
3228
+ bodyObserver?.disconnect();
3229
+ disconnectAncestorObservers();
3230
+ centerObserver?.disconnect();
3231
+ conversationObserver?.disconnect();
3232
+ trackResizeObserver.disconnect();
3233
+ observedTrackList = null;
3234
+ // 卸载时取消未执行的滚动重对齐并清空轨道上下文,避免回调落到已移除列表。
3235
+ if (trackSyncHandle !== null) cancelAnimationFrame(trackSyncHandle);
3236
+ trackSyncHandle = null;
3237
+ trackContext = null;
3238
+ trackedLayer = null;
3239
+ trackEls.clear();
3240
+ for (const [id, ghost] of [...moveGhostsById]) removeMoveGhost(id, ghost);
3241
+ for (const el of [...shiftCleanups.keys()]) cancelShift(el);
3242
+ observedCenter = null;
3243
+ toggle.removeEventListener("click", onToggleClick);
3244
+ unbindBackdrop();
3245
+ backdrop.remove();
3246
+ desktopQuery.removeEventListener("change", onResize);
3247
+ const seat = document.querySelector(CONVERSATION_SELECTOR);
3248
+ if (seat !== null && seat.parentElement !== null) {
3249
+ const center = seat.parentElement;
3250
+ center.style.flexDirection = "";
3251
+ center.style.alignItems = "";
3252
+ const flex = conversationFlexItem(center);
3253
+ if (flex !== null) {
3254
+ flex.style.flex = "";
3255
+ flex.style.minWidth = "";
3256
+ }
3257
+ }
3258
+ toggle.remove();
3259
+ document.querySelector(`[${PANE_ATTR}]`)?.remove();
3260
+ style.remove();
3261
+ if (document[INSTANCE_KEY] === cleanup) delete document[INSTANCE_KEY];
3262
+ if (globalThis[INSTANCE_KEY] === cleanup) delete globalThis[INSTANCE_KEY];
3263
+ };
3264
+ document[INSTANCE_KEY] = cleanup;
3265
+ globalThis[INSTANCE_KEY] = cleanup;
3266
+ return cleanup;
3267
+ }
3268
+
3269
+ module.exports = { name, inject, apply };