dsh-conversation-aurora 0.1.0 → 0.2.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/client.js CHANGED
@@ -2,630 +2,48 @@ window.__ModuleLoader__.load({
2
2
  id: 'dsh-conversation-aurora',
3
3
  factory() {
4
4
  /*
5
- * @local/conversation-aurora — Client 半区
5
+ * dsh-conversation-aurora — 对话区整屏动效背景(Client 半区)
6
6
  *
7
- * 4 种手势 + 2 个全局调制,全部落在同一条「列边光纤」上:
7
+ * 形态:在对话画布内部垫一层 z-index:-1 的 canvas,把整块画布当背景。
8
+ * - 只有"在干活"时才播:任何工具在跑 / 回合进行中;
9
+ * - 空闲、切会话、窗口隐藏时立刻停掉 rAF,画布淡出;
10
+ * - 每个会话按 session id 分配不同主题,Alt+T 手动轮换;
11
+ * - 文字可读性由 .dsh-stage__veil 保证:按正文列宽罩一层纱。
8
12
  *
9
- * idle 待命:两侧柔光极慢漂移,光纤只剩一条几乎看不见的呼吸发丝
10
- * read 向内:光纤上的亮段向下流动(检索 / 读文件)
11
- * web 向外:光纤心跳 + 一圈向外扩散的光晕(联网 / 派活)
12
- * write 向上:光纤自下而上逐段点亮(写码 / 跑命令)
13
- * think idle 的呼吸加快,不新增手势
14
- * wait 全体压到 ~2%、转灰蓝、缓慢向列内推
15
- * error 红色泛光,之后留一点暗红到下一次动作
16
- *
17
- * 几何:动效层挂在对话画布内部([data-conversation-content]),z-index:-1。
18
- * 左右各一个「翼」(.dsh-aurora__wing),翼宽正好等于正文列以外的空白带,
19
- * 且 overflow:hidden —— 会扩散的元素(光晕、脉冲环)都被裁在列边界之外,
20
- * 中间文字区零覆盖,这是几何保证,不是渐变赌出来的。
21
- *
22
- * 状态信号全部来自宿主已有的语义属性,不读类名:
23
- * [data-tool][data-state=running|preparing|error|stopped]
24
- * [data-chat-running] [data-streaming]
13
+ * 状态信号全部来自宿主已有语义属性,不读类名:
14
+ * [data-tool][data-state=running|preparing] [data-chat-running] [data-streaming]
25
15
  */
26
16
 
27
- /* ================= 可调常量(JS 侧) ================= */
17
+ /* ================= 可调常量 ================= */
28
18
 
29
- /** 时序与节流;视觉参数在下方 CSS 的 .dsh-aurora 块里。 */
30
19
  const TUNE = {
31
- /** 状态最短驻留:短于此值的切换会被忽略(防止连续调用工具时闪)。 */
32
- minDwellMs: 600,
33
- /** 切换瞬间光纤亮一下的时长与幅度。 */
34
- pulseMs: 420,
35
- pulseRise: 0.16,
36
- /** error 保持多久,之后转入残留暗红。 */
37
- errorHoldMs: 1300,
20
+ /** 停止后延迟多久真正淡出(防止工具间隙闪一下)。 */
21
+ holdIdleMs: 420,
38
22
  /** 最多跟踪多少个工具行(只留尾部,长会话也不拖慢)。 */
39
23
  maxTracked: 48,
40
- /** 首次挂载只回看最近多少行,避免历史里的失败行触发红光。 */
24
+ /** 首次挂载只回看最近多少行。 */
41
25
  seedRows: 24,
42
26
  /** React 重挂载引发的反复摘挂保护。 */
43
27
  remountGuard: 10,
44
28
  remountWindowMs: 2000,
45
- /** Alt+B 循环:标准 → 安静 → 关闭。 */
46
- intensity: [1, 0.55, 0],
47
- keyCode: 'b',
48
- };
49
-
50
- /** 优先级:高者可以打断低者的最短驻留。 */
51
- const MODE_PRIORITY = { idle: 0, think: 1, read: 2, web: 3, write: 4, wait: 5, error: 6 };
52
-
53
- /** 每种模式的主侧;另一侧以 60% 强度 + 0.7s 延迟跟随。 */
54
- const MODE_PRIMARY = {
55
- idle: 'both',
56
- think: 'both',
57
- read: 'left',
58
- web: 'right',
59
- write: 'left',
60
- wait: 'both',
61
- error: 'both',
29
+ /** 切会话后的静默期,避免把上一轮的残留行当成"正在工作"。 */
30
+ switchQuietMs: 600,
62
31
  };
63
32
 
64
- /** 遥测小字用的短标签。 */
65
- const MODE_TAG = {
66
- idle: 'STANDBY',
67
- think: 'THINK',
68
- read: 'SCAN',
69
- web: 'NET',
70
- write: 'BUILD',
71
- wait: 'HOLD',
72
- error: 'ERR',
73
- };
74
-
75
- /** 工具名 → 手势。表里没有的按 data-variant 归类,再兜底 read。 */
76
- const TOOL_MODE = {
77
- web_search: 'web',
78
- web_fetch: 'web',
79
- subagent: 'web',
80
- workflow: 'web',
81
- ralph: 'web',
82
- spawn_teammate: 'web',
83
- wait_agent: 'web',
84
- send_message: 'web',
85
- list_agents: 'web',
86
- grep: 'read',
87
- glob: 'read',
88
- read: 'read',
89
- read_image: 'read',
90
- lsp: 'read',
91
- session_search: 'read',
92
- session_trace: 'read',
93
- session_event_read: 'read',
94
- session_event_search: 'read',
95
- session_event_trace: 'read',
96
- cordis_inspect_list: 'read',
97
- cordis_inspect_query: 'read',
98
- cordis_inspect_self: 'read',
99
- cordis_package_inspect: 'read',
100
- cordis_runtime_inspect: 'read',
101
- job_list: 'read',
102
- job_output: 'read',
103
- get_goal: 'read',
104
- schedule_list: 'read',
105
- list_subagent_models: 'read',
106
- write: 'write',
107
- edit: 'write',
108
- run_code: 'write',
109
- pwsh: 'write',
110
- bash: 'write',
111
- terminal_open: 'write',
112
- terminal_read: 'write',
113
- terminal_list: 'write',
114
- terminal_signal: 'write',
115
- terminal_close: 'write',
116
- job_kill: 'write',
117
- create_goal: 'write',
118
- update_goal: 'write',
119
- todo_write: 'write',
120
- schedule_create: 'write',
121
- schedule_update: 'write',
122
- schedule_delete: 'write',
123
- interrupt_agent: 'write',
124
- cordis_run: 'write',
125
- cordis_stop: 'write',
126
- cordis_undefine: 'write',
127
- };
128
-
129
- const VARIANT_MODE = {
130
- web: 'web',
131
- search: 'read',
132
- read: 'read',
133
- write: 'write',
134
- edit: 'write',
135
- bash: 'write',
136
- code: 'write',
137
- others: 'write',
138
- };
139
-
140
- const NS = 'dsh-aurora';
141
- const HOST_CLASS = 'dsh-aurora-host';
142
- const FIBER_CLASS = NS + '__fiber';
33
+ const NS = 'dsh-stage';
34
+ const HOST_CLASS = 'dsh-stage-host';
143
35
  const ROW_SELECTOR = '[data-tool][data-state]';
36
+ const STORAGE = { theme: 'dsh.conversation-aurora.theme' };
144
37
 
145
- /* ================= 样式(视觉参数都在这里) ================= */
38
+ /* ================= 样式 ================= */
146
39
 
147
40
  const CSS = `
148
- .dsh-aurora-host{position:relative;isolation:isolate}
149
-
150
- /* 图层根:所有视觉参数集中在这个块,README「调参」一节有对照表。 */
151
- .dsh-aurora{
152
- --fiber-width:1.5px;
153
- --fiber-color:65,118,230; /* DeepSeek 蓝 #4176E6 */
154
- --glow-a:65,118,230;
155
- --glow-b:124,92,255; /* 柔紫 #7C5CFF */
156
- --glow-c:46,211,183; /* 青 #2ED3B7 */
157
- --glow-opacity:.22; /* 柔光强度:整条空白带的底色 */
158
- --wash-color:78,98,150; /* 底光色:低饱和冷灰蓝,像一层浅浅的面板 */
159
- --fiber-opacity:.16; /* 静息发丝亮度 */
160
- --gesture-opacity:.5; /* 手势亮度:检索的亮段 / 写码的生长条 */
161
- --pulse-opacity:.4; /* 联网时向外扩散的那圈光 */
162
- --secondary-strength:.6; /* 副侧强度 */
163
- --hud-ink:15,17,21; /* HUD 墨色:近黑,像印在纸上的仪器标记 */
164
- --hud-opacity:1; /* HUD 层的总开关:角标 / 遥测 / 扫描纹 / 数据柱 */
165
- --side-delay:0ms;
166
- --crossfade:700ms;
167
- --intensity:1;
168
- --aurora-shift:0px; /* 滚动条造成的列偏移,由 JS 量出来 */
169
- --drift-slow:40s; /* 两团柔光的周期,互不成整数比 */
170
- --drift-fast:52s;
171
- position:absolute;
172
- inset:0;
173
- z-index:8; /* 盖过输入区那条白色渐变,才能铺到窗口底边;拖宽手柄同为 8 且在 DOM 之后,仍在上面 */
174
- overflow:hidden;
175
- pointer-events:none;
176
- transition:opacity 600ms ease, visibility 0s linear 0s;
177
- }
178
-
179
- /* 空闲 = 什么都没有:整层淡出并停掉动画,两侧回到普通白纸。
180
- 一旦有输出或工具在跑(think / read / web / write / wait / error)才重新浮出来。 */
181
- .dsh-aurora[data-mode=idle]{
182
- opacity:0;
183
- visibility:hidden;
184
- transition:opacity 600ms ease, visibility 0s linear 600ms;
185
- }
186
- .dsh-aurora[data-mode=idle] *{animation-play-state:paused}
187
-
188
- .dsh-aurora[data-off]{display:none}
189
-
190
- body[data-ds-dark-theme] .dsh-aurora{
191
- --fiber-color:110,160,255;
192
- --glow-a:110,160,255;
193
- --glow-b:160,130,255;
194
- --glow-c:80,225,200;
195
- --glow-opacity:.3;
196
- --wash-color:40,62,110;
197
- --fiber-opacity:.22;
198
- --gesture-opacity:.56;
199
- --pulse-opacity:.46;
200
- --hud-ink:190,214,255;
201
- }
202
-
203
- /* ---------- 翼:正文列之外的空白带,最外层的裁切框 ---------- */
204
-
205
- .dsh-aurora__wing{
206
- position:absolute;
207
- top:0;
208
- bottom:0;
209
- overflow:hidden;
210
- pointer-events:none;
211
- }
212
- .dsh-aurora__wing[data-side=left]{
213
- left:0;
214
- width:max(0px, calc((100% - var(--dsh-chat-content-width, 748px)) / 2 - var(--aurora-shift, 0px)));
215
- --side:1;
216
- --push:26px;
217
- }
218
- .dsh-aurora__wing[data-side=right]{
219
- right:0;
220
- width:max(0px, calc((100% - var(--dsh-chat-content-width, 748px)) / 2 + var(--aurora-shift, 0px)));
221
- --side:1;
222
- --push:-26px;
223
- }
224
-
225
- /* ---------- 柔光:铺满整条空白带,向列边界方向淡出 ---------- */
226
-
227
- .dsh-aurora__glow{
228
- position:absolute;
229
- inset:0;
230
- overflow:hidden;
231
- opacity:calc(var(--glow-opacity) * var(--side) * var(--intensity));
232
- transition:opacity var(--crossfade) ease;
233
- }
234
- .dsh-aurora__wing[data-side=left] .dsh-aurora__glow{
235
- -webkit-mask-image:linear-gradient(to right,#000 0 55%,#0000 100%);
236
- mask-image:linear-gradient(to right,#000 0 55%,#0000 100%);
237
- }
238
- .dsh-aurora__wing[data-side=right] .dsh-aurora__glow{
239
- -webkit-mask-image:linear-gradient(to left,#000 0 55%,#0000 100%);
240
- mask-image:linear-gradient(to left,#000 0 55%,#0000 100%);
241
- }
242
-
243
- /* 底光:贯穿整条带子的静态渐变,保证中间不会空掉 */
244
- /* 底光:从窗口边淡向正文列,让空白带有一点纵深,但不变成一块色板 */
245
- .dsh-aurora__wash{
246
- position:absolute;
247
- inset:0;
248
- background:linear-gradient(to right,rgba(var(--wash-color),.5),rgba(var(--wash-color),0) 88%);
249
- }
250
- .dsh-aurora__wing[data-side=right] .dsh-aurora__wash{
251
- background:linear-gradient(to left,rgba(var(--wash-color),.5),rgba(var(--wash-color),0) 88%);
252
- }
253
-
254
- /* 漂移光团:尺寸按色带自身的宽高算,画布再高也不会只剩两个角 */
255
- .dsh-aurora__blob{
256
- position:absolute;
257
- border-radius:50%;
258
- }
259
- .dsh-aurora__blob[data-k=a]{
260
- width:150%;
261
- height:62%;
262
- background:radial-gradient(ellipse at 50% 50%, rgba(var(--glow-a),.6), rgba(var(--glow-a),0) 68%);
263
- }
264
- .dsh-aurora__blob[data-k=b]{
265
- width:165%;
266
- height:56%;
267
- background:radial-gradient(ellipse at 50% 50%, rgba(var(--glow-c),.55), rgba(var(--glow-c),0) 70%);
268
- }
269
- .dsh-aurora__wing[data-side=right] .dsh-aurora__blob[data-k=a]{background-image:radial-gradient(ellipse at 50% 50%, rgba(var(--glow-b),.62), rgba(var(--glow-b),0) 68%)}
270
- .dsh-aurora__wing[data-side=right] .dsh-aurora__blob[data-k=b]{background-image:radial-gradient(ellipse at 50% 50%, rgba(var(--glow-a),.5), rgba(var(--glow-a),0) 70%)}
271
-
272
- .dsh-aurora__wing[data-side=left] .dsh-aurora__blob[data-k=a]{top:-16%;left:-34%;animation:aurora-drift-a var(--drift-slow) ease-in-out infinite}
273
- .dsh-aurora__wing[data-side=left] .dsh-aurora__blob[data-k=b]{bottom:-14%;right:-34%;animation:aurora-drift-b var(--drift-fast) ease-in-out infinite;animation-delay:-13s}
274
- .dsh-aurora__wing[data-side=right] .dsh-aurora__blob[data-k=a]{top:-12%;right:-36%;animation:aurora-drift-b var(--drift-fast) ease-in-out infinite;animation-delay:-7s}
275
- .dsh-aurora__wing[data-side=right] .dsh-aurora__blob[data-k=b]{bottom:-12%;left:-34%;animation:aurora-drift-a var(--drift-slow) ease-in-out infinite;animation-delay:-21s}
276
-
277
- /* ---------- 列边光纤:所有手势的载体,离正文列 20px ---------- */
278
-
279
- .dsh-aurora__fiber{
280
- position:absolute;
281
- top:0;
282
- bottom:0;
283
- width:var(--fiber-width);
284
- animation-delay:var(--side-delay);
285
- }
286
- .dsh-aurora__wing[data-side=left] .dsh-aurora__fiber{right:20px}
287
- .dsh-aurora__wing[data-side=right] .dsh-aurora__fiber{left:20px}
288
-
289
- .dsh-aurora__fiber i{
290
- position:absolute;
291
- inset:0;
292
- display:block;
293
- transition:opacity var(--crossfade) ease;
294
- animation-delay:var(--side-delay);
295
- }
296
-
297
- /* 静息发丝:几乎看不见,靠呼吸幅度让人察觉 */
298
- .dsh-aurora__base{
299
- opacity:calc(var(--fiber-opacity) * var(--side) * var(--intensity));
300
- background:linear-gradient(to bottom,#0000,rgb(var(--fiber-color)) 12% 88%,#0000);
301
- }
302
-
303
- /* 检索:柔和的亮段向下流动(首尾都透明,循环处不会跳) */
304
- .dsh-aurora__scan{
305
- top:-56px;
306
- bottom:-56px;
307
- opacity:0;
308
- background-image:repeating-linear-gradient(to bottom,
309
- rgba(var(--fiber-color),0) 0px,
310
- rgba(var(--fiber-color),0) 12px,
311
- rgba(var(--fiber-color),.9) 20px,
312
- rgba(var(--fiber-color),.9) 36px,
313
- rgba(var(--fiber-color),0) 46px,
314
- rgba(var(--fiber-color),0) 56px);
315
- }
316
-
317
- /* 写码:从下往上长出来 */
318
- .dsh-aurora__fill{
319
- opacity:0;
320
- transform-origin:50% 100%;
321
- background-image:linear-gradient(to top,rgb(var(--fiber-color)),#0000 80%);
322
- }
323
-
324
- /* 联网:向外扩散的一圈光 */
325
- .dsh-aurora__pulse{
326
- position:absolute;
327
- top:50%;
328
- width:280px;
329
- height:280px;
330
- margin-top:-140px;
331
- border-radius:50%;
332
- opacity:0;
333
- pointer-events:none;
334
- background:radial-gradient(circle,#0000 52%,rgba(var(--glow-b),1) 58%,#0000 66%);
335
- animation-delay:var(--side-delay);
336
- }
337
- .dsh-aurora__wing[data-side=left] .dsh-aurora__pulse{
338
- right:20px;
339
- margin-right:-140px;
340
- -webkit-mask-image:linear-gradient(to right,#000 0 52%,#0000 58%);
341
- mask-image:linear-gradient(to right,#000 0 52%,#0000 58%);
342
- }
343
- .dsh-aurora__wing[data-side=right] .dsh-aurora__pulse{
344
- left:20px;
345
- margin-left:-140px;
346
- -webkit-mask-image:linear-gradient(to left,#000 0 52%,#0000 58%);
347
- mask-image:linear-gradient(to left,#000 0 52%,#0000 58%);
348
- }
349
-
350
- /* ---------- HUD:让两侧读起来像一块仪表,而不是一团渐变 ---------- */
351
-
352
- /* 扫描纹:静态 CRT 质感,只铺在空白带里 */
353
- .dsh-aurora__grid{
354
- position:absolute;
355
- inset:0;
356
- background-image:
357
- repeating-linear-gradient(to bottom,rgba(var(--hud-ink),.04) 0 1px,#0000 1px 4px),
358
- repeating-linear-gradient(to right,rgba(var(--hud-ink),.05) 0 1px,#0000 1px 96px);
359
- opacity:calc(var(--hud-opacity) * var(--side) * var(--intensity));
360
- transition:opacity var(--crossfade) ease;
361
- }
362
- body[data-ds-dark-theme] .dsh-aurora__grid{
363
- background-image:
364
- repeating-linear-gradient(to bottom,rgba(var(--hud-ink),.05) 0 1px,#0000 1px 4px),
365
- repeating-linear-gradient(to right,rgba(var(--hud-ink),.06) 0 1px,#0000 1px 96px);
366
- }
367
- .dsh-aurora__wing[data-side=left] .dsh-aurora__grid{
368
- -webkit-mask-image:linear-gradient(to right,#000 0 60%,#0000 100%);
369
- mask-image:linear-gradient(to right,#000 0 60%,#0000 100%);
370
- }
371
- .dsh-aurora__wing[data-side=right] .dsh-aurora__grid{
372
- -webkit-mask-image:linear-gradient(to left,#000 0 60%,#0000 100%);
373
- mask-image:linear-gradient(to left,#000 0 60%,#0000 100%);
374
- }
375
-
376
- /* 取景角标:把正文列框成一块"屏幕" */
377
- .dsh-aurora__corner{
378
- position:absolute;
379
- width:20px;
380
- height:20px;
381
- border:0 solid rgba(var(--hud-ink),.55);
382
- opacity:calc(var(--hud-opacity) * var(--side) * var(--intensity));
383
- transition:opacity var(--crossfade) ease;
384
- }
385
- .dsh-aurora__wing[data-side=left] .dsh-aurora__corner{right:20px}
386
- .dsh-aurora__wing[data-side=right] .dsh-aurora__corner{left:20px}
387
- .dsh-aurora__wing[data-side=left] .dsh-aurora__corner[data-at=top]{top:14px;border-top-width:1.5px;border-right-width:1.5px}
388
- .dsh-aurora__wing[data-side=left] .dsh-aurora__corner[data-at=bottom]{bottom:14px;border-bottom-width:1.5px;border-right-width:1.5px}
389
- .dsh-aurora__wing[data-side=right] .dsh-aurora__corner[data-at=top]{top:14px;border-top-width:1.5px;border-left-width:1.5px}
390
- .dsh-aurora__wing[data-side=right] .dsh-aurora__corner[data-at=bottom]{bottom:14px;border-bottom-width:1.5px;border-left-width:1.5px}
391
-
392
- /* 扫描光带:一直在扫,有动作时更亮 */
393
- .dsh-aurora__sweep{
394
- position:absolute;
395
- left:0;
396
- right:0;
397
- top:-10px;
398
- height:1.5px;
399
- opacity:calc(.22 * var(--hud-opacity) * var(--side) * var(--intensity));
400
- background:linear-gradient(to right,rgba(var(--hud-ink),0),rgba(var(--hud-ink),.55),rgba(var(--hud-ink),0));
401
- transition:opacity var(--crossfade) ease;
402
- animation:aurora-sweep 3.4s linear infinite;
403
- animation-delay:var(--side-delay);
404
- }
405
- .dsh-aurora[data-mode=think] .dsh-aurora__sweep,
406
- .dsh-aurora[data-mode=read] .dsh-aurora__sweep,
407
- .dsh-aurora[data-mode=web] .dsh-aurora__sweep,
408
- .dsh-aurora[data-mode=write] .dsh-aurora__sweep{
409
- opacity:calc(.55 * var(--hud-opacity) * var(--side) * var(--intensity));
410
- }
411
- .dsh-aurora[data-mode=wait] .dsh-aurora__sweep,
412
- .dsh-aurora[data-mode=error] .dsh-aurora__sweep{opacity:0}
413
-
414
- /* 数据柱:一直在流,只是"动手"时更亮更快 */
415
- .dsh-aurora__bars{
416
- position:absolute;
417
- inset:0;
418
- overflow:hidden;
419
- opacity:calc(.45 * var(--hud-opacity) * var(--side) * var(--intensity));
420
- transition:opacity var(--crossfade) ease;
421
- }
422
- .dsh-aurora[data-mode=write] .dsh-aurora__bars{
423
- opacity:calc(.7 * var(--hud-opacity) * var(--side) * var(--intensity));
424
- }
425
- .dsh-aurora__bars i{
426
- position:absolute;
427
- bottom:-90px;
428
- width:1px;
429
- height:var(--h,42px);
430
- background:repeating-linear-gradient(to top,rgba(var(--fiber-color),.85) 0 4px,#0000 4px 10px);
431
- -webkit-mask-image:linear-gradient(to top,#0000,#000 18%,#000 82%,#0000);
432
- mask-image:linear-gradient(to top,#0000,#000 18%,#000 82%,#0000);
433
- animation:aurora-rise var(--dur,5s) linear infinite;
434
- animation-delay:var(--del,0s);
435
- }
436
- .dsh-aurora[data-mode=write] .dsh-aurora__bars i{animation-duration:calc(var(--dur,5s) * .55)}
437
- .dsh-aurora__bars i:nth-child(1){left:5%;--h:34px;--dur:4.6s;--del:-1.1s}
438
- .dsh-aurora__bars i:nth-child(2){left:16%;--h:62px;--dur:6.2s;--del:-3.4s}
439
- .dsh-aurora__bars i:nth-child(3){left:27%;--h:26px;--dur:3.9s;--del:-0.6s}
440
- .dsh-aurora__bars i:nth-child(4){left:38%;--h:74px;--dur:7.1s;--del:-5.2s}
441
- .dsh-aurora__bars i:nth-child(5){left:50%;--h:44px;--dur:5.4s;--del:-2.3s}
442
- .dsh-aurora__bars i:nth-child(6){left:62%;--h:30px;--dur:4.2s;--del:-3.9s}
443
- .dsh-aurora__bars i:nth-child(7){left:74%;--h:58px;--dur:6.6s;--del:-1.8s}
444
- .dsh-aurora__bars i:nth-child(8){left:87%;--h:38px;--dur:5.1s;--del:-4.6s}
445
-
446
- /* 遥测:竖排小字,读起来像设备读数 */
447
- .dsh-aurora__telemetry{
448
- position:absolute;
449
- bottom:22px;
450
- z-index:2;
451
- font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
452
- font-size:9px;
453
- line-height:1;
454
- letter-spacing:.2em;
455
- color:rgba(var(--hud-ink),.62);
456
- writing-mode:vertical-rl;
457
- white-space:nowrap;
458
- opacity:0;
459
- transition:opacity var(--crossfade) ease;
460
- pointer-events:none;
461
- }
462
- .dsh-aurora__telemetry b{
463
- font-weight:500;
464
- display:inline-block;
465
- padding:0 2px;
466
- }
467
- .dsh-aurora__telemetry b + b{margin-inline-start:.85em}
468
- .dsh-aurora__telemetry .dsh-aurora__t-hex{color:rgba(var(--hud-ink),.34)}
469
- .dsh-aurora__telemetry .dsh-aurora__t-cur{animation:aurora-blink 1.2s steps(2,start) infinite}
470
- @keyframes aurora-blink{0%,100%{opacity:1}50%{opacity:.12}}
471
- .dsh-aurora__wing[data-side=left] .dsh-aurora__telemetry{left:10px;transform:rotate(180deg)}
472
- .dsh-aurora__wing[data-side=right] .dsh-aurora__telemetry{right:10px}
473
- .dsh-aurora:not([data-mode=idle]) .dsh-aurora__telemetry{
474
- opacity:calc(var(--hud-opacity) * var(--side) * var(--intensity));
475
- }
476
-
477
- /* glitch:状态切换和活动期间偶发一次,青/品红分离 + 轻微错位 */
478
- .dsh-aurora__fiber::before,
479
- .dsh-aurora__fiber::after{
480
- content:"";
481
- position:absolute;
482
- inset:0;
483
- opacity:0;
484
- pointer-events:none;
485
- transition:opacity 90ms linear;
486
- }
487
- .dsh-aurora__fiber::before{
488
- transform:translateX(-2px);
489
- background:linear-gradient(to bottom,#0000,rgba(0,229,255,.95) 12% 88%,#0000);
490
- }
491
- .dsh-aurora__fiber::after{
492
- transform:translateX(2px);
493
- background:linear-gradient(to bottom,#0000,rgba(255,0,132,.9) 12% 88%,#0000);
494
- }
495
- .dsh-aurora[data-glitch] .dsh-aurora__fiber::before{opacity:.6}
496
- .dsh-aurora[data-glitch] .dsh-aurora__fiber::after{opacity:.5}
497
- .dsh-aurora[data-glitch] .dsh-aurora__corner,
498
- .dsh-aurora[data-glitch] .dsh-aurora__bars{animation:aurora-jitter .14s steps(2,end) 1}
499
- @keyframes aurora-jitter{
500
- 0%{transform:translate3d(0,0,0)}
501
- 50%{transform:translate3d(1.5px,0,0)}
502
- 100%{transform:translate3d(0,0,0)}
503
- }
504
-
505
- /* ---------- 主侧 / 副侧:只改强度与相位,不改语义 ---------- */
506
-
507
- .dsh-aurora[data-primary=left] .dsh-aurora__wing[data-side=right],
508
- .dsh-aurora[data-primary=right] .dsh-aurora__wing[data-side=left]{
509
- --side:var(--secondary-strength);
510
- --side-delay:700ms;
511
- }
512
-
513
- /* ---------- 手势 ---------- */
514
-
515
- .dsh-aurora[data-mode=idle] .dsh-aurora__base{animation:aurora-breathe 9s ease-in-out infinite}
516
- .dsh-aurora[data-mode=think] .dsh-aurora__base{animation:aurora-breathe 3s ease-in-out infinite}
517
-
518
- .dsh-aurora[data-mode=read] .dsh-aurora__scan{
519
- --g:calc(var(--gesture-opacity) * var(--side) * var(--intensity));
520
- opacity:var(--g);
521
- animation:aurora-scan 1.1s linear infinite;
522
- }
523
- .dsh-aurora[data-mode=write] .dsh-aurora__fill{
524
- --g:calc(var(--gesture-opacity) * var(--side) * var(--intensity));
525
- animation:aurora-build 1.2s ease-in-out infinite;
526
- }
527
- .dsh-aurora[data-mode=web] .dsh-aurora__pulse{
528
- --g:calc(var(--pulse-opacity) * var(--side) * var(--intensity));
529
- animation:aurora-pulse 1.6s ease-out infinite;
530
- }
531
- .dsh-aurora[data-mode=web] .dsh-aurora__base{animation:aurora-breathe 1.1s ease-in-out infinite}
532
-
533
- /* ---------- 全局调制 ---------- */
534
-
535
- .dsh-aurora[data-mode=wait]{
536
- --glow-opacity:.018;
537
- --fiber-opacity:.02;
538
- --fiber-color:150,170,200;
539
- }
540
- .dsh-aurora[data-mode=wait] .dsh-aurora__base{animation:aurora-wait 4s ease-in-out infinite}
541
- .dsh-aurora[data-mode=wait] .dsh-aurora__scan,
542
- .dsh-aurora[data-mode=wait] .dsh-aurora__fill,
543
- .dsh-aurora[data-mode=wait] .dsh-aurora__pulse{opacity:0;animation:none}
544
-
545
- .dsh-aurora[data-mode=error],
546
- .dsh-aurora[data-tint=error]{--fiber-color:229,72,77}
547
- .dsh-aurora[data-mode=error]{--glow-a:229,72,77;--glow-b:229,72,77;--fiber-opacity:.08}
548
- .dsh-aurora[data-mode=error] .dsh-aurora__base{animation:aurora-breathe 1.2s ease-in-out infinite}
549
-
550
- /* ---------- 动画:只碰 transform / opacity ---------- */
551
-
552
- @keyframes aurora-drift-a{
553
- 0%{transform:translate3d(0,0,0) scale(1)}
554
- 50%{transform:translate3d(7%,-9%,0) scale(1.12)}
555
- 100%{transform:translate3d(0,0,0) scale(1)}
556
- }
557
- @keyframes aurora-drift-b{
558
- 0%{transform:translate3d(0,0,0) scale(1.1)}
559
- 50%{transform:translate3d(-6%,7%,0) scale(.94)}
560
- 100%{transform:translate3d(0,0,0) scale(1.1)}
561
- }
562
- @keyframes aurora-breathe{
563
- 0%,100%{opacity:calc(var(--fiber-opacity) * var(--side) * var(--intensity) * 1.2)}
564
- 50%{opacity:calc(var(--fiber-opacity) * var(--side) * var(--intensity) * 3.2)}
565
- }
566
- @keyframes aurora-scan{
567
- from{transform:translate3d(0,-56px,0)}
568
- to{transform:translate3d(0,0,0)}
569
- }
570
- @keyframes aurora-build{
571
- 0%{transform:scaleY(0);opacity:var(--g)}
572
- 70%{transform:scaleY(1);opacity:var(--g)}
573
- 100%{transform:scaleY(1);opacity:0}
574
- }
575
- @keyframes aurora-pulse{
576
- 0%{transform:scale(.35);opacity:var(--g)}
577
- 70%{opacity:calc(var(--g) * .22)}
578
- 100%{transform:scale(1.7);opacity:0}
579
- }
580
- @keyframes aurora-wait{
581
- 0%,100%{transform:translate3d(0,0,0)}
582
- 50%{transform:translate3d(var(--push),0,0)}
583
- }
584
- @keyframes aurora-sweep{
585
- from{transform:translate3d(0,0,0)}
586
- to{transform:translate3d(0,106vh,0)}
587
- }
588
- @keyframes aurora-rise{
589
- from{transform:translate3d(0,0,0)}
590
- to{transform:translate3d(0,-125vh,0)}
591
- }
592
-
593
- /* ---------- 降级 ---------- */
594
-
595
- .dsh-aurora[data-paused],
596
- .dsh-aurora[data-paused] *{animation-play-state:paused !important}
597
-
598
- @media (prefers-reduced-motion:reduce){
599
- .dsh-aurora,
600
- .dsh-aurora *{animation:none !important;transition:none !important}
601
- .dsh-stage{transition:none !important}
602
- .dsh-aurora__glow{opacity:calc(var(--glow-opacity) * var(--side) * var(--intensity) * 1.5)}
603
- .dsh-aurora__base{opacity:calc(var(--fiber-opacity) * var(--side) * var(--intensity) * 2)}
604
- .dsh-aurora[data-mode=read] .dsh-aurora__scan{
605
- opacity:calc(var(--gesture-opacity) * var(--side) * .6);
606
- transform:translate3d(0,-28px,0);
607
- }
608
- .dsh-aurora[data-mode=write] .dsh-aurora__fill{
609
- opacity:calc(var(--gesture-opacity) * var(--side) * .55);
610
- transform:scaleY(.45);
611
- }
612
- .dsh-aurora[data-mode=web] .dsh-aurora__pulse{
613
- opacity:calc(var(--pulse-opacity) * var(--side) * .7);
614
- transform:scale(.9);
615
- }
616
- .dsh-aurora[data-mode=wait]{--glow-opacity:.02;--fiber-opacity:.02}
617
- }
618
-
619
- /* ==========================================================================
620
- * 预设 B:整块画布当背景(stage)
621
- * 与预设 A 完全独立:自己的根元素、自己的 canvas、自己的开关,
622
- * 互斥显示,所以 A 的任何行为都不会被这套影响。
623
- * ========================================================================== */
41
+ .dsh-stage-host{position:relative;isolation:isolate}
624
42
 
625
43
  .dsh-stage{
626
44
  position:absolute;
627
45
  inset:0;
628
- z-index:-1; /* 在正文之下,所以文字永远压在上面 */
46
+ z-index:-1; /* 在正文之下:文字永远压在背景上 */
629
47
  overflow:hidden;
630
48
  pointer-events:none;
631
49
  opacity:0;
@@ -634,7 +52,7 @@ body[data-ds-dark-theme] .dsh-aurora__grid{
634
52
  .dsh-stage[data-on]{opacity:1}
635
53
  .dsh-stage canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
636
54
 
637
- /* 中间的纸面:按正文列宽罩住文字区,两侧留亮,让背景整体可读又不糊成一片 */
55
+ /* 中间的纸面:按正文列宽罩住文字区,两侧留亮 */
638
56
  .dsh-stage__veil{
639
57
  position:absolute;
640
58
  inset:-1px;
@@ -655,21 +73,58 @@ body[data-ds-dark-theme] .dsh-stage__veil{
655
73
  rgba(14,16,21,.4) calc(50% + var(--dsh-chat-content-width, 748px) / 2 + 110px),
656
74
  rgba(14,16,21,0) 100%);
657
75
  }
76
+
77
+ @media (prefers-reduced-motion:reduce){
78
+ .dsh-stage{transition:none !important}
79
+ }
658
80
  `;
659
81
 
660
82
  /* ================= 工具函数 ================= */
661
83
 
84
+ function readStore(key, fallback) {
85
+ try {
86
+ return window.localStorage.getItem(key) || fallback;
87
+ } catch {
88
+ return fallback;
89
+ }
90
+ }
91
+
92
+ function writeStore(key, value) {
93
+ try {
94
+ window.localStorage.setItem(key, value);
95
+ } catch {
96
+ /* 隐私模式下只在本次会话生效 */
97
+ }
98
+ }
99
+
100
+ function hashString(text) {
101
+ let h = 2166136261;
102
+ for (let i = 0; i < text.length; i += 1) {
103
+ h ^= text.charCodeAt(i);
104
+ h = Math.imul(h, 16777619);
105
+ }
106
+ return h >>> 0;
107
+ }
108
+
109
+ /** 浅色用墨色 + multiply;深色用亮色 + lighter。 */
110
+ function stagePalette() {
111
+ const dark = document.body.hasAttribute('data-ds-dark-theme');
112
+ return dark
113
+ ? { a: '120,190,255', b: '170,140,255', c: '110,235,215', bg: 'rgba(12,14,19,0.14)', blend: 'lighter' }
114
+ : { a: '38,78,205', b: '112,44,214', c: '10,132,120', bg: 'rgba(255,255,255,0.14)', blend: 'multiply' };
115
+ }
116
+
662
117
  function addStyle() {
663
118
  const style = document.createElement('style');
664
- style.dataset.pluginCss = '@local/conversation-aurora/aurora.css';
119
+ style.dataset.pluginCss = 'dsh-conversation-aurora/stage.css';
665
120
  style.textContent = CSS;
666
121
  document.head.appendChild(style);
667
122
  return style;
668
123
  }
669
124
 
670
125
  /**
671
- * 找对话画布:优先用宿主稳定的语义属性,其次退回「从输入框往上找不透明背景」。
672
- * 同时存在多个会话面板时取可见面积最大的那个。找不到就返回 null,调用方静默跳过。
126
+ * 找对话画布:优先用宿主稳定的语义属性,同时存在多个时取可见面积最大的;
127
+ * 找不到就退回「从输入框往上找不透明背景」。
673
128
  */
674
129
  function findCanvas() {
675
130
  const all = document.querySelectorAll('[data-conversation-content]');
@@ -699,84 +154,17 @@ body[data-ds-dark-theme] .dsh-stage__veil{
699
154
  return null;
700
155
  }
701
156
 
702
- function createLayer() {
703
- const layer = document.createElement('div');
704
- layer.className = NS;
705
- layer.setAttribute('aria-hidden', 'true');
706
- layer.dataset.mode = 'idle';
707
- layer.dataset.primary = 'both';
708
- const wing = (side) =>
709
- '<div class="' + NS + '__wing" data-side="' + side + '">' +
710
- '<div class="' + NS + '__glow"><i class="' + NS + '__wash"></i><i class="' + NS + '__blob" data-k="a"></i><i class="' + NS + '__blob" data-k="b"></i></div>' +
711
- '<div class="' + NS + '__grid"></div>' +
712
- '<div class="' + NS + '__bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>' +
713
- '<div class="' + NS + '__sweep"></div>' +
714
- '<div class="' + NS + '__corner" data-at="top"></div>' +
715
- '<div class="' + NS + '__corner" data-at="bottom"></div>' +
716
- '<div class="' + NS + '__telemetry">' +
717
- '<b class="' + NS + '__t-tag"></b>' +
718
- '<b class="' + NS + '__t-tool"></b>' +
719
- '<b class="' + NS + '__t-hex"></b>' +
720
- '<b class="' + NS + '__t-cur">\u258C</b>' +
721
- '</div>' +
722
- '<div class="' + FIBER_CLASS + '">' +
723
- '<i class="' + NS + '__base"></i>' +
724
- '<i class="' + NS + '__fill"></i>' +
725
- '<i class="' + NS + '__scan"></i>' +
726
- '</div>' +
727
- '<div class="' + NS + '__pulse"></div>' +
728
- '</div>';
729
- layer.innerHTML = wing('left') + wing('right');
730
- return layer;
731
- }
732
-
733
- function toolMode(tool, variant) {
734
- return TOOL_MODE[tool] || VARIANT_MODE[variant || ''] || 'read';
735
- }
736
-
737
- /* ================= 预设 B:整屏背景的主题库 ================= */
738
-
739
- const STORAGE = {
740
- preset: 'dsh.conversation-aurora.preset',
741
- theme: 'dsh.conversation-aurora.theme',
742
- };
743
-
744
- function readStore(key, fallback) {
745
- try {
746
- return window.localStorage.getItem(key) || fallback;
747
- } catch {
748
- return fallback;
749
- }
750
- }
751
-
752
- function writeStore(key, value) {
753
- try {
754
- window.localStorage.setItem(key, value);
755
- } catch {
756
- /* 隐私模式等拿不到存储时,就只在本次会话生效 */
757
- }
758
- }
759
-
760
- function hashString(text) {
761
- let h = 2166136261;
762
- for (let i = 0; i < text.length; i += 1) {
763
- h ^= text.charCodeAt(i);
764
- h = Math.imul(h, 16777619);
765
- }
766
- return h >>> 0;
157
+ function createStage() {
158
+ const el = document.createElement('div');
159
+ el.className = NS;
160
+ el.setAttribute('aria-hidden', 'true');
161
+ el.innerHTML = '<canvas></canvas><div class="' + NS + '__veil"></div>';
162
+ return el;
767
163
  }
768
164
 
769
- function stagePalette() {
770
- const dark = document.body.hasAttribute('data-ds-dark-theme');
771
- // 浅色:用更饱和的“墨色”,配合 multiply(正片叠底)画在白纸上
772
- // 深色:亮色 + lighter(加色),才会有发光感
773
- return dark
774
- ? { a: '120,190,255', b: '170,140,255', c: '110,235,215', bg: 'rgba(12,14,19,0.14)', blend: 'lighter' }
775
- : { a: '38,78,205', b: '112,44,214', c: '10,132,120', bg: 'rgba(255,255,255,0.14)', blend: 'multiply' };
776
- }
165
+ /* ================= 主题库 ================= */
777
166
 
778
- /** 星空跃迁:星点从中心向外加速飞出。 */
779
- function themeWarp(g) {
167
+ function themeWarp(g) {
780
168
  const spawn = (z) => ({
781
169
  x: Math.random() * 2 - 1,
782
170
  y: Math.random() * 2 - 1,
@@ -1349,24 +737,14 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1349
737
  { id: 'bubbles', name: '肥皂泡', create: themeBubbles },
1350
738
  ];
1351
739
 
1352
- function createStage() {
1353
- const el = document.createElement('div');
1354
- el.className = 'dsh-stage';
1355
- el.setAttribute('aria-hidden', 'true');
1356
- el.innerHTML = '<canvas></canvas><div class="dsh-stage__veil"></div>';
1357
- return el;
1358
- }
1359
-
1360
740
  /* ================= 安装 / 拆卸 ================= */
1361
741
 
1362
742
  function install() {
1363
743
  if (typeof document === 'undefined' || typeof window === 'undefined') return function () {};
1364
744
 
1365
745
  const style = addStyle();
1366
- const layer = createLayer();
1367
746
  const stage = createStage();
1368
747
  const tracked = new Set();
1369
- const errorSeen = new WeakSet();
1370
748
  const media = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
1371
749
 
1372
750
  let host = null;
@@ -1374,53 +752,124 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1374
752
  let pending = 0;
1375
753
  let disposed = false;
1376
754
  let gaveUp = false;
1377
- let primed = false;
1378
755
  let booted = false;
1379
- let currentMode = 'idle';
1380
- let lastSwitch = 0;
1381
- let errorUntil = 0;
756
+ let busy = false;
757
+ let idleTimer = 0;
1382
758
  let remountCount = 0;
1383
759
  let remountAt = 0;
1384
- let intensity = 0;
1385
- let errorTimer = 0;
1386
- let currentTool = '';
1387
760
  let sessionKey = null;
1388
- let suppressUntil = 0;
1389
- let preset = readStore(STORAGE.preset, 'edge') === 'stage' ? 'stage' : 'edge';
761
+ let quietUntil = 0;
1390
762
  let forcedTheme = readStore(STORAGE.theme, '');
1391
763
  let stageRaf = 0;
1392
764
  let stageTheme = null;
1393
- let hexTimer = 0;
1394
- let hexSeed = 0x2f3a;
1395
- let hexTicks = 0;
1396
- let glitchTimer = 0;
1397
765
 
1398
766
  const reduced = () => media !== null && media.matches;
1399
767
 
768
+ /* ---------- 挂载 ---------- */
769
+
770
+ function ensureHost() {
771
+ if (disposed || gaveUp) return;
772
+ if (host && host.isConnected && host.contains(stage)) return;
773
+
774
+ const canvas = findCanvas();
775
+ if (!canvas) return;
776
+ if (canvas === host && canvas.contains(stage)) return;
777
+
778
+ const now = Date.now();
779
+ if (now - remountAt > TUNE.remountWindowMs) {
780
+ remountAt = now;
781
+ remountCount = 0;
782
+ }
783
+ remountCount += 1;
784
+ if (remountCount > TUNE.remountGuard) {
785
+ gaveUp = true;
786
+ return;
787
+ }
788
+
789
+ const previous = host;
790
+ if (host && host !== canvas) host.classList.remove(HOST_CLASS);
791
+ host = canvas;
792
+ host.classList.add(HOST_CLASS);
793
+ if (stage.parentNode !== host) host.insertBefore(stage, host.firstChild);
794
+ if (previous !== canvas) resetForSwitch(previous !== null);
795
+ }
796
+
1400
797
  /**
1401
- * 切换会话:上一轮的工具行可能还挂在 DOM 上,历史里的失败行也会被当成"新事件"。
1402
- * 这里把跟踪表清空、重新"预热",并静默一小段时间,等宿主把新会话渲染稳定。
798
+ * 切会话:上一轮的工具行可能还挂着,历史里的失败行也会被当成新事件。
799
+ * 清空跟踪表、静默一小段时间,等新会话渲染稳定。
1403
800
  */
1404
- function resetForSwitch(withSuppress) {
801
+ function resetForSwitch(quiet) {
1405
802
  tracked.clear();
1406
- primed = false;
1407
- errorUntil = 0;
1408
- currentTool = '';
1409
- suppressUntil = withSuppress ? Date.now() + 700 : 0;
803
+ busy = false;
804
+ if (idleTimer) {
805
+ window.clearTimeout(idleTimer);
806
+ idleTimer = 0;
807
+ }
808
+ quietUntil = quiet ? Date.now() + TUNE.switchQuietMs : 0;
1410
809
  sessionKey = host ? host.getAttribute('data-conversation-session') || '' : null;
1411
- currentMode = '';
1412
- applyMode('idle');
1413
- syncTelemetry('idle');
1414
- syncStage('idle');
810
+ syncStage();
1415
811
  }
1416
812
 
1417
- /* ---------- 预设 B:整屏背景 ---------- */
813
+ /* ---------- 工具行跟踪 ---------- */
1418
814
 
1419
- function applyPresetClass() {
1420
- layer.style.display = preset === 'edge' ? '' : 'none';
1421
- stage.style.display = preset === 'stage' ? '' : 'none';
815
+ function track(node) {
816
+ if (!node || node.nodeType !== 1) return;
817
+ if (node.matches && node.matches(ROW_SELECTOR)) tracked.add(node);
818
+ if (!node.querySelectorAll) return;
819
+ const inner = node.querySelectorAll(ROW_SELECTOR);
820
+ for (let i = 0; i < inner.length; i += 1) tracked.add(inner[i]);
1422
821
  }
1423
822
 
823
+ function prune() {
824
+ for (const el of tracked) {
825
+ if (!el.isConnected) tracked.delete(el);
826
+ }
827
+ while (tracked.size > TUNE.maxTracked) {
828
+ tracked.delete(tracked.values().next().value);
829
+ }
830
+ }
831
+
832
+ function seed() {
833
+ const scope = document.querySelector('[data-conversation-scroll]') || document;
834
+ const all = scope.querySelectorAll(ROW_SELECTOR);
835
+ const from = Math.max(0, all.length - TUNE.seedRows);
836
+ for (let i = from; i < all.length; i += 1) tracked.add(all[i]);
837
+ }
838
+
839
+ /* ---------- 状态判定:只关心"在不在干活" ---------- */
840
+
841
+ function detectBusy() {
842
+ for (const el of tracked) {
843
+ if (!el.isConnected) continue;
844
+ const state = el.getAttribute('data-state');
845
+ if (state === 'running' || state === 'preparing') return true;
846
+ }
847
+ return document.querySelector('[data-chat-running], [data-streaming]') !== null;
848
+ }
849
+
850
+ function setBusy(next) {
851
+ if (disposed) return;
852
+ if (next) {
853
+ if (idleTimer) {
854
+ window.clearTimeout(idleTimer);
855
+ idleTimer = 0;
856
+ }
857
+ if (!busy) {
858
+ busy = true;
859
+ syncStage();
860
+ }
861
+ return;
862
+ }
863
+ if (!busy || idleTimer) return;
864
+ idleTimer = window.setTimeout(() => {
865
+ idleTimer = 0;
866
+ busy = false;
867
+ syncStage();
868
+ }, TUNE.holdIdleMs);
869
+ }
870
+
871
+ /* ---------- 背景播放 ---------- */
872
+
1424
873
  function stageThemeId() {
1425
874
  for (let i = 0; i < STAGE_THEMES.length; i += 1) {
1426
875
  if (forcedTheme && STAGE_THEMES[i].id === forcedTheme) return forcedTheme;
@@ -1437,9 +886,10 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1437
886
 
1438
887
  function startStage() {
1439
888
  stopStage();
1440
- if (!host || preset !== 'stage') return;
889
+ if (!host) return;
1441
890
  const canvas = stage.querySelector('canvas');
1442
891
  if (!canvas) return;
892
+
1443
893
  const rect = host.getBoundingClientRect();
1444
894
  const dpr = Math.min(2, window.devicePixelRatio || 1);
1445
895
  const w = Math.max(1, Math.round(rect.width));
@@ -1449,6 +899,7 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1449
899
  const ctx = canvas.getContext('2d');
1450
900
  if (!ctx) return;
1451
901
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
902
+
1452
903
  const pal = stagePalette();
1453
904
  ctx.globalCompositeOperation = pal.blend;
1454
905
 
@@ -1459,10 +910,10 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1459
910
  }
1460
911
  stageTheme = factory.create({ ctx, w, h, pal });
1461
912
 
1462
- // 预热若干帧:第一帧不空,静止降级时也有完整画面
913
+ // 预热若干帧:第一帧不空,减少动态效果时也有完整静止画面
1463
914
  for (let i = 0; i < 24; i += 1) stageTheme.step(16);
1464
-
1465
915
  if (reduced()) return;
916
+
1466
917
  let last = 0;
1467
918
  const frame = (now) => {
1468
919
  if (!stageRaf) return;
@@ -1478,9 +929,9 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1478
929
  stageRaf = window.requestAnimationFrame(frame);
1479
930
  }
1480
931
 
1481
- /** 只有"在干活"时才播;空闲、隐藏、或者不在这个预设里,就彻底停下。 */
1482
- function syncStage(mode) {
1483
- const want = preset === 'stage' && mode !== 'idle' && !document.hidden && !disposed;
932
+ /** 只有"在干活 + 窗口可见"才播;其它情况彻底停下。 */
933
+ function syncStage() {
934
+ const want = busy && !document.hidden && !disposed && Date.now() >= quietUntil;
1484
935
  if (!want) {
1485
936
  delete stage.dataset.on;
1486
937
  stopStage();
@@ -1490,13 +941,6 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1490
941
  if (!stageRaf) startStage();
1491
942
  }
1492
943
 
1493
- function togglePreset() {
1494
- preset = preset === 'edge' ? 'stage' : 'edge';
1495
- writeStore(STORAGE.preset, preset);
1496
- applyPresetClass();
1497
- syncStage(currentMode);
1498
- }
1499
-
1500
944
  function cycleTheme() {
1501
945
  const ids = [];
1502
946
  for (let i = 0; i < STAGE_THEMES.length; i += 1) ids.push(STAGE_THEMES[i].id);
@@ -1504,228 +948,13 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1504
948
  if (at < 0) at = ids.indexOf(stageThemeId());
1505
949
  forcedTheme = ids[(at + 1) % ids.length];
1506
950
  writeStore(STORAGE.theme, forcedTheme);
1507
- if (preset === 'stage') {
951
+ if (stageRaf || busy) {
1508
952
  stopStage();
1509
- syncStage(currentMode);
953
+ if (busy && !document.hidden) startStage();
1510
954
  }
1511
955
  }
1512
956
 
1513
- /* ---------- 挂载 ---------- */
1514
-
1515
- /**
1516
- * 正文列在滚动容器里居中,但滚动容器右侧有滚动条 + 2px 外边距,
1517
- * 于是列中心比画布中心偏左一点。量出来补到 --aurora-shift,
1518
- * 让左右光纤到列的距离真正相等。
1519
- */
1520
- function updateShift() {
1521
- if (!host) return;
1522
- try {
1523
- const scroller = host.querySelector('[data-conversation-scroll]');
1524
- if (!scroller) return;
1525
- const shift = (scroller.offsetWidth - scroller.clientWidth + 2) / 2;
1526
- layer.style.setProperty('--aurora-shift', shift.toFixed(1) + 'px');
1527
- } catch {
1528
- /* 量不到就按 0 处理,最多差几像素 */
1529
- }
1530
- }
1531
-
1532
- function ensureHost() {
1533
- if (disposed || gaveUp) return;
1534
- // 快路径:宿主还在、图层还挂着,就不要每帧重新找画布(避免强制布局)。
1535
- if (host && host.isConnected && host.contains(layer)) return;
1536
-
1537
- const canvas = findCanvas();
1538
- if (!canvas) return;
1539
- if (canvas === host && canvas.contains(layer)) return;
1540
-
1541
- const now = Date.now();
1542
- if (now - remountAt > TUNE.remountWindowMs) {
1543
- remountAt = now;
1544
- remountCount = 0;
1545
- }
1546
- remountCount += 1;
1547
- if (remountCount > TUNE.remountGuard) {
1548
- // 宿主在和我们抢节点:放弃,宁可没有效果也不要抖动。
1549
- gaveUp = true;
1550
- return;
1551
- }
1552
-
1553
- const previous = host;
1554
- if (host && host !== canvas) host.classList.remove(HOST_CLASS);
1555
- host = canvas;
1556
- host.classList.add(HOST_CLASS);
1557
- if (layer.parentNode !== host) host.insertBefore(layer, host.firstChild);
1558
- if (stage.parentNode !== host) host.insertBefore(stage, layer.nextSibling);
1559
- updateShift();
1560
- applyPresetClass();
1561
- if (previous !== canvas) resetForSwitch(previous !== null);
1562
- }
1563
-
1564
- /* ---------- 跟踪工具行 ---------- */
1565
-
1566
- function track(node) {
1567
- if (!node || node.nodeType !== 1) return;
1568
- if (node.matches && node.matches(ROW_SELECTOR)) tracked.add(node);
1569
- if (!node.querySelectorAll) return;
1570
- const inner = node.querySelectorAll(ROW_SELECTOR);
1571
- for (let i = 0; i < inner.length; i += 1) tracked.add(inner[i]);
1572
- }
1573
-
1574
- function prune() {
1575
- for (const el of tracked) {
1576
- if (!el.isConnected) tracked.delete(el);
1577
- }
1578
- while (tracked.size > TUNE.maxTracked) {
1579
- tracked.delete(tracked.values().next().value);
1580
- }
1581
- }
1582
-
1583
- function seed() {
1584
- const scope = document.querySelector('[data-conversation-scroll]') || document;
1585
- const all = scope.querySelectorAll(ROW_SELECTOR);
1586
- const from = Math.max(0, all.length - TUNE.seedRows);
1587
- for (let i = from; i < all.length; i += 1) tracked.add(all[i]);
1588
- }
1589
-
1590
- /* ---------- 状态判定 ---------- */
1591
-
1592
- function detectError() {
1593
- let fresh = false;
1594
- for (const el of tracked) {
1595
- if (!el.isConnected) continue;
1596
- const state = el.getAttribute('data-state');
1597
- if (state !== 'error' && state !== 'stopped') continue;
1598
- if (errorSeen.has(el)) continue;
1599
- errorSeen.add(el);
1600
- if (primed) fresh = true;
1601
- }
1602
- return fresh;
1603
- }
1604
-
1605
- function resolveMode() {
1606
- currentTool = '';
1607
- const rows = [];
1608
- for (const el of tracked) if (el.isConnected) rows.push(el);
1609
- for (let i = rows.length - 1; i >= 0; i -= 1) {
1610
- const el = rows[i];
1611
- const state = el.getAttribute('data-state');
1612
- if (state !== 'running' && state !== 'preparing') continue;
1613
- const tool = el.getAttribute('data-tool') || '';
1614
- currentTool = tool;
1615
- if (tool === 'ask_user_question') return 'wait';
1616
- return toolMode(tool, el.getAttribute('data-variant'));
1617
- }
1618
- if (document.querySelector('[data-chat-running], [data-streaming]')) return 'think';
1619
- return 'idle';
1620
- }
1621
-
1622
- function applyMode(next) {
1623
- if (disposed || !host) return;
1624
- const now = Date.now();
1625
- if (next === currentMode) return;
1626
- if (now - lastSwitch < TUNE.minDwellMs && MODE_PRIORITY[next] <= MODE_PRIORITY[currentMode]) {
1627
- // 被最短驻留挡住时,补一次定时重判,否则可能一直停在旧状态(没有后续 DOM 变更)
1628
- if (!pending) {
1629
- pending = window.setTimeout(scan, TUNE.minDwellMs - (now - lastSwitch) + 10);
1630
- }
1631
- return;
1632
- }
1633
-
1634
- currentMode = next;
1635
- lastSwitch = now;
1636
- layer.dataset.mode = next;
1637
- layer.dataset.primary = MODE_PRIMARY[next] || 'both';
1638
-
1639
- if (next === 'error') layer.dataset.tint = 'error';
1640
- else if (next !== 'idle') delete layer.dataset.tint;
1641
-
1642
- if (next !== 'idle') flash();
1643
- syncStage(next);
1644
- }
1645
-
1646
- /** 一次 130ms 的 glitch:光纤青/品红分离 + HUD 轻微错位。 */
1647
- function glitch() {
1648
- if (disposed || document.hidden || reduced() || layer.dataset.off === '1') return;
1649
- layer.dataset.glitch = '1';
1650
- if (glitchTimer) window.clearTimeout(glitchTimer);
1651
- glitchTimer = window.setTimeout(() => {
1652
- glitchTimer = 0;
1653
- delete layer.dataset.glitch;
1654
- }, 130);
1655
- }
1656
-
1657
- /** 遥测里的十六进制读数:有动作时才跳,静止时一个定时器都不留。 */
1658
- function startHex() {
1659
- if (hexTimer || document.hidden) return;
1660
- const el = layer.querySelector('.' + NS + '__t-hex');
1661
- if (!el) return;
1662
- const paint = () => {
1663
- hexSeed = (hexSeed * 1103515245 + 12345) & 0x7fffffff;
1664
- el.textContent = '0x' + (hexSeed & 0xffff).toString(16).toUpperCase().padStart(4, '0');
1665
- hexTicks += 1;
1666
- if (hexTicks % 10 === 0) glitch();
1667
- };
1668
- paint();
1669
- hexTimer = window.setInterval(paint, 600);
1670
- }
1671
-
1672
- function stopHex() {
1673
- if (!hexTimer) return;
1674
- window.clearInterval(hexTimer);
1675
- hexTimer = 0;
1676
- }
1677
-
1678
- /** 同步竖排遥测文字:只在内容真的变了时才写 DOM。 */
1679
- function syncTelemetry(mode) {
1680
- try {
1681
- const tag = layer.querySelector('.' + NS + '__t-tag');
1682
- const toolEl = layer.querySelector('.' + NS + '__t-tool');
1683
- const tagText = MODE_TAG[mode] || '';
1684
- const toolText = mode === 'idle' || mode === 'wait' ? '' : String(currentTool || '').slice(0, 14).toUpperCase();
1685
- if (tag && tag.textContent !== tagText) tag.textContent = tagText;
1686
- if (toolEl && toolEl.textContent !== toolText) toolEl.textContent = toolText;
1687
- if (mode === 'idle') stopHex();
1688
- else startHex();
1689
- } catch {
1690
- /* 遥测只是装饰,失败不影响主效果 */
1691
- }
1692
- }
1693
-
1694
- /** 状态切换时给光纤一次单次亮脉冲(WAAPI,不占常驻动画)。 */
1695
- function flash() {
1696
- if (!host || document.hidden || reduced() || layer.dataset.off === '1') return;
1697
- glitch();
1698
- const bases = layer.querySelectorAll('.' + NS + '__base');
1699
- for (let i = 0; i < bases.length; i += 1) {
1700
- const base = bases[i];
1701
- const from = Number.parseFloat(window.getComputedStyle(base).opacity) || 0;
1702
- const peak = Math.min(1, from + TUNE.pulseRise);
1703
- try {
1704
- const anim = base.animate(
1705
- [{ opacity: String(from) }, { opacity: String(peak) }, { opacity: String(from) }],
1706
- { duration: TUNE.pulseMs, easing: 'cubic-bezier(.2,.7,.2,1)' },
1707
- );
1708
- anim.addEventListener('finish', () => anim.cancel());
1709
- } catch {
1710
- /* 老引擎不支持 WAAPI 时不闪,不影响其它状态 */
1711
- }
1712
- }
1713
- // 角标一次轻微位移:像镜头重新对上焦
1714
- const corners = layer.querySelectorAll('.' + NS + '__corner');
1715
- for (let i = 0; i < corners.length; i += 1) {
1716
- try {
1717
- const anim = corners[i].animate(
1718
- [{ transform: 'translateX(0)' }, { transform: 'translateX(2px)' }, { transform: 'translateX(0)' }],
1719
- { duration: 150, easing: 'ease-out' },
1720
- );
1721
- anim.addEventListener('finish', () => anim.cancel());
1722
- } catch {
1723
- /* 同上 */
1724
- }
1725
- }
1726
- }
1727
-
1728
- /* ---------- 每帧扫描 ---------- */
957
+ /* ---------- 扫描 ---------- */
1729
958
 
1730
959
  function scan() {
1731
960
  pending = 0;
@@ -1734,44 +963,24 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1734
963
  ensureHost();
1735
964
  if (!host) return;
1736
965
 
1737
- // 切换会话:等新会话挂载稳定再判断,避免把上一轮/历史行当成"正在工作"
1738
966
  const key = host.getAttribute('data-conversation-session') || '';
1739
967
  if (sessionKey === null) sessionKey = key;
1740
968
  else if (key !== sessionKey) {
1741
969
  resetForSwitch(true);
1742
970
  return;
1743
971
  }
1744
- if (Date.now() < suppressUntil) {
1745
- applyMode('idle');
1746
- syncTelemetry('idle');
972
+ if (Date.now() < quietUntil) {
973
+ syncStage();
1747
974
  return;
1748
975
  }
1749
976
 
1750
977
  prune();
1751
- const fresh = detectError();
1752
- const now = Date.now();
1753
- if (fresh) {
1754
- errorUntil = now + TUNE.errorHoldMs;
1755
- // 如果没有后续变更,红光也必须自己退掉,否则会一直停在错误态。
1756
- if (errorTimer) window.clearTimeout(errorTimer);
1757
- errorTimer = window.setTimeout(() => {
1758
- errorTimer = 0;
1759
- requestScan();
1760
- }, TUNE.errorHoldMs + 40);
1761
- }
1762
- primed = true;
1763
- const mode = now < errorUntil ? 'error' : resolveMode();
1764
- applyMode(mode);
1765
- syncTelemetry(mode);
978
+ setBusy(detectBusy());
1766
979
  } catch {
1767
980
  /* 任何一步失败都只是不生效,不影响宿主 */
1768
981
  }
1769
982
  }
1770
983
 
1771
- /**
1772
- * 用短定时器而不是 rAF 合并高频变更:状态判定不需要帧精度,
1773
- * 而 rAF 在窗口被遮挡 / 无头环境里会被节流甚至停摆,导致状态卡住。
1774
- */
1775
984
  function requestScan() {
1776
985
  if (disposed || pending) return;
1777
986
  pending = window.setTimeout(scan, 48);
@@ -1804,37 +1013,24 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1804
1013
 
1805
1014
  function onVisibility() {
1806
1015
  if (document.hidden) {
1807
- layer.dataset.paused = '1';
1808
- stopHex();
1809
1016
  stopStage();
1810
1017
  } else {
1811
- delete layer.dataset.paused;
1018
+ syncStage();
1812
1019
  requestScan();
1813
- syncStage(currentMode);
1814
1020
  }
1815
1021
  }
1816
1022
 
1817
1023
  function onResize() {
1818
- updateShift();
1024
+ if (stageRaf) {
1025
+ stopStage();
1026
+ startStage();
1027
+ }
1819
1028
  }
1820
1029
 
1821
1030
  function onKeyDown(event) {
1822
1031
  if (!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
1823
- const key = String(event.key || '').toLowerCase();
1824
- if (key === 'm') {
1825
- togglePreset();
1826
- return;
1827
- }
1828
- if (key === 't') {
1829
- cycleTheme();
1830
- return;
1831
- }
1832
- if (key !== TUNE.keyCode) return;
1833
- intensity = (intensity + 1) % TUNE.intensity.length;
1834
- const value = TUNE.intensity[intensity];
1835
- layer.style.setProperty('--intensity', String(value));
1836
- if (value === 0) layer.dataset.off = '1';
1837
- else delete layer.dataset.off;
1032
+ if (String(event.key || '').toLowerCase() !== TUNE.themeKey) return;
1033
+ cycleTheme();
1838
1034
  }
1839
1035
 
1840
1036
  /* ---------- 启动 ---------- */
@@ -1843,11 +1039,10 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1843
1039
  if (disposed || booted) return;
1844
1040
  booted = true;
1845
1041
  ensureHost();
1846
- applyPresetClass();
1847
1042
  try {
1848
1043
  seed();
1849
1044
  } catch {
1850
- /* 会话还没渲染出来时没有可跟踪的行,正常 */
1045
+ /* 会话还没渲染出来时没有可跟踪的行 */
1851
1046
  }
1852
1047
  requestScan();
1853
1048
  observe();
@@ -1866,9 +1061,7 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1866
1061
  if (disposed) return;
1867
1062
  disposed = true;
1868
1063
  if (pending) window.clearTimeout(pending);
1869
- if (errorTimer) window.clearTimeout(errorTimer);
1870
- if (glitchTimer) window.clearTimeout(glitchTimer);
1871
- stopHex();
1064
+ if (idleTimer) window.clearTimeout(idleTimer);
1872
1065
  if (observer) observer.disconnect();
1873
1066
  document.removeEventListener('DOMContentLoaded', boot);
1874
1067
  document.removeEventListener('visibilitychange', onVisibility);
@@ -1876,7 +1069,6 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1876
1069
  window.removeEventListener('keydown', onKeyDown, true);
1877
1070
  stopStage();
1878
1071
  if (stage.parentNode) stage.parentNode.removeChild(stage);
1879
- if (layer.parentNode) layer.parentNode.removeChild(layer);
1880
1072
  if (host) host.classList.remove(HOST_CLASS);
1881
1073
  style.remove();
1882
1074
  };
@@ -1885,9 +1077,8 @@ body[data-ds-dark-theme] .dsh-stage__veil{
1885
1077
  /* ================= 插件入口 ================= */
1886
1078
 
1887
1079
  /**
1888
- * Alt+D 切换浅色/深色。
1889
- * 走宿主的 theme 服务(setTheme),不直接改 body 属性,所以和「设置 → 外观」始终一致。
1890
- * theme 服务不在时不激活这个键,插件其余部分不受影响。
1080
+ * Alt+T 换背景主题。
1081
+ * Alt+D 切浅色/深色:走宿主 theme 服务,不直接改 body,和「设置 → 外观」始终一致。
1891
1082
  */
1892
1083
  function bindThemeHotkey(ctx) {
1893
1084
  if (!ctx || typeof ctx.inject !== 'function') return;