dsh-notify-me 1.1.4 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -1
- package/lib/client.js +1162 -815
- package/package.json +95 -95
package/lib/client.js
CHANGED
|
@@ -1,815 +1,1162 @@
|
|
|
1
|
-
// dsh-notify-me — browser half (client plugin bundle).
|
|
2
|
-
//
|
|
3
|
-
// Loaded by dsh-client-modules at /plugins/dsh-notify-me/client.js and
|
|
4
|
-
// executed through the vendored cordis Loader's lazy-CJS module table
|
|
5
|
-
// (window.__ModuleLoader__.load). The factory body is plain CJS with
|
|
6
|
-
// require() resolved against the shell's module table — the same shape the
|
|
7
|
-
// shipped ui-* packages' tsdown bundles emit. react is part of the platform
|
|
8
|
-
// base, so require("react") resolves to the shell-seeded instance (same as
|
|
9
|
-
// every shipped ui-* bundle); the settings page is skipped when react or the
|
|
10
|
-
// slots/locale services are absent, keeping the reminder core self-contained.
|
|
11
|
-
//
|
|
12
|
-
// What this plugin does (all client-side, no settings transport needed):
|
|
13
|
-
// - watches
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
var
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
function
|
|
179
|
-
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
var
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
var
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1
|
+
// dsh-notify-me — browser half (client plugin bundle).
|
|
2
|
+
//
|
|
3
|
+
// Loaded by dsh-client-modules at /plugins/dsh-notify-me/client.js and
|
|
4
|
+
// executed through the vendored cordis Loader's lazy-CJS module table
|
|
5
|
+
// (window.__ModuleLoader__.load). The factory body is plain CJS with
|
|
6
|
+
// require() resolved against the shell's module table — the same shape the
|
|
7
|
+
// shipped ui-* packages' tsdown bundles emit. react is part of the platform
|
|
8
|
+
// base, so require("react") resolves to the shell-seeded instance (same as
|
|
9
|
+
// every shipped ui-* bundle); the settings page is skipped when react or the
|
|
10
|
+
// slots/locale services are absent, keeping the reminder core self-contained.
|
|
11
|
+
//
|
|
12
|
+
// What this plugin does (all client-side, no settings transport needed):
|
|
13
|
+
// - watches two services, because pending interactions and the running bit
|
|
14
|
+
// have different owners on current hosts:
|
|
15
|
+
// * "uiSession" -> pendingInteractions (a Map<sessionId, interaction>
|
|
16
|
+
// snapshot whose values carry kind 'approval' | 'plan-review' |
|
|
17
|
+
// 'question'). Present since DSH 0.1.2-alpha.2; the controller
|
|
18
|
+
// snapshots stopped carrying `pending` in the same release, so this is
|
|
19
|
+
// the only live source of "the agent is waiting on you" on 0.1.2+.
|
|
20
|
+
// It is resolved through ctx.get() — cordis throws on a plain
|
|
21
|
+
// ctx.uiSession read for any name absent from the plugin's inject map;
|
|
22
|
+
// * "sessions" -> the selected session's ConversationSnapshot (running)
|
|
23
|
+
// and the session-list summaries of every other session (running /
|
|
24
|
+
// completed), which carry the "reply finished" edge on every version;
|
|
25
|
+
// - fires a Windows desktop Notification (+ optional sound) when the agent
|
|
26
|
+
// needs the user's input — pending interaction kinds 'approval',
|
|
27
|
+
// 'plan-review' or 'question' — and when a reply/turn finishes;
|
|
28
|
+
// - registers a "通知提醒 / Notify me" page into DSH Settings
|
|
29
|
+
// (settings.section slot) exposing an on/off master switch, per-channel
|
|
30
|
+
// toggles, volume and the notification language (follow-interface /
|
|
31
|
+
// Simplified Chinese / English);
|
|
32
|
+
// - preferences persist in localStorage (origin-scoped, like other
|
|
33
|
+
// third-party client plugins) and are tweakable on window.__dshNotifyMe.
|
|
34
|
+
|
|
35
|
+
window.__ModuleLoader__.load({
|
|
36
|
+
id: "dsh-notify-me",
|
|
37
|
+
factory: (require) => {
|
|
38
|
+
var module = { exports: {} };
|
|
39
|
+
var exports = module.exports;
|
|
40
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
41
|
+
|
|
42
|
+
// react is externalized into the loader module table; never required by
|
|
43
|
+
// the reminder core, only by the optional Settings page.
|
|
44
|
+
var React = null;
|
|
45
|
+
try { React = require("react") || null; } catch (e) { React = null; }
|
|
46
|
+
|
|
47
|
+
// ───────────────────────── configuration ─────────────────────────
|
|
48
|
+
var LS_CONFIG = "dshNotifyMe.config";
|
|
49
|
+
var DEFAULTS = {
|
|
50
|
+
enabled: true, // master switch: off = no toast/sound/title marker at all
|
|
51
|
+
// language of the notification copy: 'auto' follows the DSH UI locale,
|
|
52
|
+
// 'zh' and 'en' pin Simplified Chinese / English.
|
|
53
|
+
language: "auto",
|
|
54
|
+
// toast + sound are only useful while the user is elsewhere; the tab
|
|
55
|
+
// being hidden is the reliable "away" signal.
|
|
56
|
+
attentionHiddenOnly: false, // needs-input alerts even while the page is visible
|
|
57
|
+
// A wait inside the conversation already on screen needs no toast: the
|
|
58
|
+
// approval card is right there, and a toast lands on top of it. Leaving
|
|
59
|
+
// the page brings that alert back (see flushQuieted).
|
|
60
|
+
currentHiddenOnly: true, // the visible session's own waits stay silent
|
|
61
|
+
doneHiddenOnly: true, // reply-finished alerts only when the page is hidden
|
|
62
|
+
toast: true,
|
|
63
|
+
sound: true,
|
|
64
|
+
volume: 0.5,
|
|
65
|
+
autoFocus: true // clicking the toast focuses the DSH window
|
|
66
|
+
};
|
|
67
|
+
function readConfig() {
|
|
68
|
+
var cfg = {};
|
|
69
|
+
try {
|
|
70
|
+
var raw = localStorage.getItem(LS_CONFIG);
|
|
71
|
+
if (raw) cfg = JSON.parse(raw);
|
|
72
|
+
} catch (e) { /* corrupt storage -> defaults */ }
|
|
73
|
+
for (var k in DEFAULTS) if (!(k in cfg)) cfg[k] = DEFAULTS[k];
|
|
74
|
+
return cfg;
|
|
75
|
+
}
|
|
76
|
+
function writeConfig(patch) {
|
|
77
|
+
var cfg = readConfig();
|
|
78
|
+
for (var k in patch) cfg[k] = patch[k];
|
|
79
|
+
try { localStorage.setItem(LS_CONFIG, JSON.stringify(cfg)); } catch (e) {}
|
|
80
|
+
return cfg;
|
|
81
|
+
}
|
|
82
|
+
var config = readConfig();
|
|
83
|
+
// Persist a patch, swap the live module config and re-apply any active
|
|
84
|
+
// title marker (its prefix follows the notification language).
|
|
85
|
+
function setConfigLocal(patch) {
|
|
86
|
+
config = writeConfig(patch);
|
|
87
|
+
applyTitle();
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ───────────────────────── copy tables ─────────────────────────
|
|
92
|
+
// Notification copy, keyed by language id. Selected through
|
|
93
|
+
// effectiveLang() so a pinned language stays pinned even while the DSH UI
|
|
94
|
+
// speaks the other one.
|
|
95
|
+
var TXT = {
|
|
96
|
+
zh: {
|
|
97
|
+
kind: { approval: "审批请求", "plan-review": "方案待确认", question: "提问" },
|
|
98
|
+
kindDefault: "需要操作",
|
|
99
|
+
bodyKind: { approval: "有审批待处理", "plan-review": "方案待确认", question: "有提问待回答" },
|
|
100
|
+
bodyDefault: "需要你操作",
|
|
101
|
+
current: "当前对话",
|
|
102
|
+
doneTitle: "DSH · 回复完成",
|
|
103
|
+
doneBody: "回复已完成",
|
|
104
|
+
background: "后台会话",
|
|
105
|
+
marker: "🔔 需要你 · ",
|
|
106
|
+
sep: ":",
|
|
107
|
+
testAttention: "这是一条测试提醒:DSH 需要你的操作",
|
|
108
|
+
testDone: "测试提醒 · 回复完成示例"
|
|
109
|
+
},
|
|
110
|
+
en: {
|
|
111
|
+
kind: { approval: "Approval request", "plan-review": "Plan review", question: "Question" },
|
|
112
|
+
kindDefault: "Action needed",
|
|
113
|
+
bodyKind: {
|
|
114
|
+
approval: "An approval is waiting for you",
|
|
115
|
+
"plan-review": "A plan awaits your review",
|
|
116
|
+
question: "A question is waiting for you"
|
|
117
|
+
},
|
|
118
|
+
bodyDefault: "Your input is needed",
|
|
119
|
+
current: "Current conversation",
|
|
120
|
+
doneTitle: "DSH · Reply finished",
|
|
121
|
+
doneBody: "Reply finished",
|
|
122
|
+
background: "Background session",
|
|
123
|
+
marker: "🔔 Action needed · ",
|
|
124
|
+
sep: ": ",
|
|
125
|
+
testAttention: "This is a test alert: DSH is waiting for your input",
|
|
126
|
+
testDone: "Test alert · reply-finished sample"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// Locale service handle (registered on the client context by dsh-client-locale).
|
|
130
|
+
var localeSvc = null;
|
|
131
|
+
// Current DSH UI language: 'zh' | 'en' | null (unknown). Used by the
|
|
132
|
+
// 'auto' notification-language mode.
|
|
133
|
+
function currentUILang() {
|
|
134
|
+
try {
|
|
135
|
+
if (localeSvc && typeof localeSvc.getLocale === "function") {
|
|
136
|
+
var a = localeSvc.getLocale().active;
|
|
137
|
+
if (typeof a === "string" && a) {
|
|
138
|
+
if (a.indexOf("zh") === 0) return "zh";
|
|
139
|
+
if (a.indexOf("en") === 0) return "en";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
try {
|
|
144
|
+
var dl = (document.documentElement && document.documentElement.lang) || "";
|
|
145
|
+
if (dl.indexOf("zh") === 0) return "zh";
|
|
146
|
+
if (dl.indexOf("en") === 0) return "en";
|
|
147
|
+
} catch (e) {}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function effectiveLang() {
|
|
151
|
+
if (!config.language || config.language === "auto") return currentUILang() || "zh";
|
|
152
|
+
return config.language === "en" ? "en" : "zh";
|
|
153
|
+
}
|
|
154
|
+
function texts() { return TXT[effectiveLang()] || TXT.zh; }
|
|
155
|
+
|
|
156
|
+
// ─────────────────────── notification plumbing ───────────────────
|
|
157
|
+
var permAsked = false;
|
|
158
|
+
function requestPermission() {
|
|
159
|
+
if (!("Notification" in window) || permAsked) return;
|
|
160
|
+
permAsked = true;
|
|
161
|
+
try {
|
|
162
|
+
if (Notification.permission === "default") {
|
|
163
|
+
Notification.requestPermission().catch(function () {});
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {}
|
|
166
|
+
}
|
|
167
|
+
function canToast() {
|
|
168
|
+
if (!config.toast) return false;
|
|
169
|
+
try {
|
|
170
|
+
return "Notification" in window && Notification.permission === "granted";
|
|
171
|
+
} catch (e) { return false; } // a policy-disabled Notification must not break delivery
|
|
172
|
+
}
|
|
173
|
+
// Bring the window forward and make the alerted session the current one,
|
|
174
|
+
// so a click on the toast lands on the conversation that needs the user
|
|
175
|
+
// instead of just the app. Every failure here is non-fatal: a session that
|
|
176
|
+
// left the list, an older host without `open()`, or a cordis service that
|
|
177
|
+
// cannot be resolved must still leave a usable window in front.
|
|
178
|
+
function focusSession(sessionId) {
|
|
179
|
+
try { window.focus(); } catch (e) {}
|
|
180
|
+
if (!sessionId) return;
|
|
181
|
+
try {
|
|
182
|
+
var svc = lookupService("sessions");
|
|
183
|
+
if (!svc || typeof svc.open !== "function") return;
|
|
184
|
+
var st = listStore && listStore.getSnapshot();
|
|
185
|
+
if (st && st.byId && !st.byId[sessionId] && st.current !== sessionId) return;
|
|
186
|
+
svc.open(sessionId);
|
|
187
|
+
} catch (e) {}
|
|
188
|
+
}
|
|
189
|
+
function showToast(kind, title, body, sessionId) {
|
|
190
|
+
if (!canToast()) return;
|
|
191
|
+
try {
|
|
192
|
+
var n = new Notification(title, {
|
|
193
|
+
body: body || "",
|
|
194
|
+
tag: "dsh-notify-me-" + kind,
|
|
195
|
+
renotify: true,
|
|
196
|
+
icon: (location.origin || "") + "/favicon.svg"
|
|
197
|
+
});
|
|
198
|
+
if (config.autoFocus) {
|
|
199
|
+
n.onclick = function () {
|
|
200
|
+
focusSession(sessionId);
|
|
201
|
+
n.close();
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
setTimeout(function () { n.close(); }, 15000);
|
|
205
|
+
} catch (e) {}
|
|
206
|
+
}
|
|
207
|
+
// Sound: WebAudio beeps (no asset needed). The context only runs after a
|
|
208
|
+
// user gesture; we unlock it on the first pointer/key event.
|
|
209
|
+
var audioCtx = null;
|
|
210
|
+
function unlockAudio() {
|
|
211
|
+
try {
|
|
212
|
+
if (!audioCtx) {
|
|
213
|
+
var AC = window.AudioContext || window.webkitAudioContext;
|
|
214
|
+
if (!AC) return;
|
|
215
|
+
audioCtx = new AC();
|
|
216
|
+
}
|
|
217
|
+
if (audioCtx.state === "suspended") audioCtx.resume().catch(function () {});
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
}
|
|
220
|
+
function tone(freq, startIn, durMs, vol) {
|
|
221
|
+
if (!audioCtx || audioCtx.state !== "running") return;
|
|
222
|
+
try {
|
|
223
|
+
var t0 = audioCtx.currentTime + startIn;
|
|
224
|
+
var osc = audioCtx.createOscillator();
|
|
225
|
+
var gain = audioCtx.createGain();
|
|
226
|
+
osc.type = "sine";
|
|
227
|
+
osc.frequency.value = freq;
|
|
228
|
+
gain.gain.setValueAtTime(0.0001, t0);
|
|
229
|
+
gain.gain.exponentialRampToValueAtTime(vol, t0 + 0.02);
|
|
230
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + durMs / 1000);
|
|
231
|
+
osc.connect(gain).connect(audioCtx.destination);
|
|
232
|
+
osc.start(t0);
|
|
233
|
+
osc.stop(t0 + durMs / 1000 + 0.05);
|
|
234
|
+
} catch (e) {}
|
|
235
|
+
}
|
|
236
|
+
function playAttentionSound() {
|
|
237
|
+
if (!config.sound) return;
|
|
238
|
+
unlockAudio();
|
|
239
|
+
tone(880, 0.0, 140, config.volume);
|
|
240
|
+
tone(1174, 0.16, 140, config.volume);
|
|
241
|
+
tone(1568, 0.32, 200, config.volume);
|
|
242
|
+
}
|
|
243
|
+
function playDoneSound() {
|
|
244
|
+
if (!config.sound) return;
|
|
245
|
+
unlockAudio();
|
|
246
|
+
tone(659, 0.0, 150, config.volume);
|
|
247
|
+
tone(988, 0.18, 260, config.volume);
|
|
248
|
+
}
|
|
249
|
+
function unlockOnce() {
|
|
250
|
+
unlockAudio();
|
|
251
|
+
requestPermission();
|
|
252
|
+
}
|
|
253
|
+
function onFirstGesture() {
|
|
254
|
+
unlockOnce();
|
|
255
|
+
window.removeEventListener("pointerdown", onFirstGesture, true);
|
|
256
|
+
window.removeEventListener("keydown", onFirstGesture, true);
|
|
257
|
+
}
|
|
258
|
+
window.addEventListener("pointerdown", onFirstGesture, true);
|
|
259
|
+
window.addEventListener("keydown", onFirstGesture, true);
|
|
260
|
+
|
|
261
|
+
// ────────────────────────── title marker ─────────────────────────
|
|
262
|
+
// The host owns document.title (it tracks the session/workspace name), so
|
|
263
|
+
// the base title is re-read on every apply instead of being frozen once —
|
|
264
|
+
// and the exact string we last wrote is remembered, so the host taking the
|
|
265
|
+
// title over (or an earlier load leaving a marker behind) can never make us
|
|
266
|
+
// mistake our own marker for the base title.
|
|
267
|
+
var attentionActive = 0;
|
|
268
|
+
var hostBase = null; // document.title as the host last left it
|
|
269
|
+
var writtenTitle = null; // exact string this plugin last put into document.title
|
|
270
|
+
// Our own marker prefix never belongs to the base title — strip a leftover
|
|
271
|
+
// one (an earlier load that was closed while a wait was pending, or a title
|
|
272
|
+
// the host tracked while a marker was up) before storing the base.
|
|
273
|
+
function cleanBase(t) {
|
|
274
|
+
var d = texts();
|
|
275
|
+
var prefixes = [d.marker, TXT.zh.marker, TXT.en.marker];
|
|
276
|
+
for (var i = 0; i < prefixes.length; i++) {
|
|
277
|
+
while (t.indexOf(prefixes[i]) === 0) t = t.slice(prefixes[i].length);
|
|
278
|
+
}
|
|
279
|
+
return t;
|
|
280
|
+
}
|
|
281
|
+
function applyTitle() {
|
|
282
|
+
try {
|
|
283
|
+
var current = String(document.title || "");
|
|
284
|
+
if (current !== writtenTitle) hostBase = cleanBase(current);
|
|
285
|
+
var next = (config.enabled && attentionActive > 0) ? texts().marker + hostBase : hostBase;
|
|
286
|
+
writtenTitle = next;
|
|
287
|
+
if (current !== next) document.title = next;
|
|
288
|
+
} catch (e) {}
|
|
289
|
+
}
|
|
290
|
+
function acquireTitleMark() {
|
|
291
|
+
attentionActive += 1;
|
|
292
|
+
applyTitle();
|
|
293
|
+
}
|
|
294
|
+
function releaseTitleMark() {
|
|
295
|
+
if (attentionActive > 0) attentionActive -= 1;
|
|
296
|
+
applyTitle();
|
|
297
|
+
}
|
|
298
|
+
// Title marker ownership: on hosts that publish pending interactions
|
|
299
|
+
// (uiSession) that source owns the marker, but whichever source saw the wait
|
|
300
|
+
// first must still mark it (the other one may never report that key at all),
|
|
301
|
+
// so the mark is acquired by the first reporter and released only by its
|
|
302
|
+
// owner. Without this, the two sources would either double-mark or never
|
|
303
|
+
// clear the mark.
|
|
304
|
+
function markAttention(key) {
|
|
305
|
+
if (attentionKeysMarked[key]) return;
|
|
306
|
+
attentionKeysMarked[key] = true;
|
|
307
|
+
acquireTitleMark();
|
|
308
|
+
}
|
|
309
|
+
function unmarkAttention(key) {
|
|
310
|
+
delete quietedAttention[key]; // the wait is over; a queued alert dies with it
|
|
311
|
+
if (!attentionKeysMarked[key]) return;
|
|
312
|
+
delete attentionKeysMarked[key];
|
|
313
|
+
releaseTitleMark();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─────────────────────── shared alert core ───────────────────────
|
|
317
|
+
var attentionKeysFired = Object.create(null); // dedupe per pending key
|
|
318
|
+
var attentionKeysMarked = Object.create(null); // keys currently holding a title mark
|
|
319
|
+
// Waits silenced because their session is the one on screen, keyed by
|
|
320
|
+
// interaction key so an entry dies with the wait it belongs to (the mark
|
|
321
|
+
// release path deletes it) and is re-delivered once the page is backgrounded.
|
|
322
|
+
var quietedAttention = Object.create(null);
|
|
323
|
+
var lastAttentionAt = 0;
|
|
324
|
+
|
|
325
|
+
function hidden() { return document.hidden || document.visibilityState === "hidden"; }
|
|
326
|
+
|
|
327
|
+
// Deliver one alert regardless of visibility rules (used by the test
|
|
328
|
+
// buttons); respects the master switch and permission state. The title
|
|
329
|
+
// marker is NOT touched here — its ownership belongs to the watchers
|
|
330
|
+
// (markAttention / unmarkAttention), which keeps the counter balanced.
|
|
331
|
+
function deliver(kind, title, body, sessionId) {
|
|
332
|
+
if (!config.enabled) return;
|
|
333
|
+
showToast(kind, title, body, sessionId);
|
|
334
|
+
if (kind === "attention") {
|
|
335
|
+
playAttentionSound();
|
|
336
|
+
} else {
|
|
337
|
+
playDoneSound();
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
if (window.__dshNotifyMe && window.__dshNotifyMe.onEvent) {
|
|
341
|
+
window.__dshNotifyMe.onEvent(kind, { title: title, body: body, sessionId: sessionId || null });
|
|
342
|
+
}
|
|
343
|
+
} catch (e) {}
|
|
344
|
+
}
|
|
345
|
+
function fire(kind, title, body, isCurrent, sessionId) {
|
|
346
|
+
if (!config.enabled) return false;
|
|
347
|
+
if (kind === "attention" && config.attentionHiddenOnly && !hidden()) return false;
|
|
348
|
+
// The wait belongs to the conversation the user is looking at right now:
|
|
349
|
+
// the approval card is already on screen, so a toast would only cover the
|
|
350
|
+
// thing it is asking about. The caller keeps the tab marker, and
|
|
351
|
+
// flushQuieted() re-delivers the alert the moment the page is backgrounded.
|
|
352
|
+
if (kind === "attention" && quietedByVisibleSession(isCurrent)) return false;
|
|
353
|
+
if (kind === "done" && config.doneHiddenOnly && !hidden()) return false;
|
|
354
|
+
deliver(kind, title, body, sessionId);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
// True when this alert must stay silent purely because it is about the
|
|
358
|
+
// session already on screen. Kept separate from fire() so the caller can
|
|
359
|
+
// queue exactly this case and nothing else.
|
|
360
|
+
function quietedByVisibleSession(isCurrent) {
|
|
361
|
+
return !!(isCurrent && config.currentHiddenOnly && !hidden());
|
|
362
|
+
}
|
|
363
|
+
// Queued copies of the alerts that rule suppressed. Delivered by key so a
|
|
364
|
+
// wait that got answered while the user was still on the page never fires.
|
|
365
|
+
function queueQuieted(key, copy) {
|
|
366
|
+
if (key && copy) quietedAttention[key] = copy;
|
|
367
|
+
}
|
|
368
|
+
function flushQuieted() {
|
|
369
|
+
if (!config.enabled || !hidden()) return;
|
|
370
|
+
var keys = Object.keys(quietedAttention);
|
|
371
|
+
for (var i = 0; i < keys.length; i++) {
|
|
372
|
+
var copy = quietedAttention[keys[i]];
|
|
373
|
+
delete quietedAttention[keys[i]];
|
|
374
|
+
fire("attention", copy.title, copy.body, false, copy.sessionId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// A display label for a session row ("current" maps to the localized
|
|
378
|
+
// "当前对话 / Current conversation" placeholder).
|
|
379
|
+
function sessionLabelText(row) {
|
|
380
|
+
var raw = row && row.label;
|
|
381
|
+
if (!raw || raw === "current") return texts().current;
|
|
382
|
+
return raw;
|
|
383
|
+
}
|
|
384
|
+
// The session the user is looking at, or null while the list is not ready.
|
|
385
|
+
function currentSessionId() {
|
|
386
|
+
try {
|
|
387
|
+
var st = listStore && listStore.getSnapshot();
|
|
388
|
+
return (st && st.current) || null;
|
|
389
|
+
} catch (e) { return null; }
|
|
390
|
+
}
|
|
391
|
+
// Display label for a session id we only know from the interaction map:
|
|
392
|
+
// the list row's title, the localized "current" placeholder for the selected
|
|
393
|
+
// session, the raw id as a last resort.
|
|
394
|
+
function labelForSessionId(sid) {
|
|
395
|
+
try {
|
|
396
|
+
var st = listStore && listStore.getSnapshot();
|
|
397
|
+
if (st) {
|
|
398
|
+
var sum = st.byId && st.byId[sid];
|
|
399
|
+
if (sum) return sessionLabelText({ label: sum.displayTitle || sum.title || "" });
|
|
400
|
+
if (st.current === sid) return texts().current;
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
return sid;
|
|
404
|
+
}
|
|
405
|
+
// Returns null once the alert went out, or the copy it would have shown
|
|
406
|
+
// when only the visible-session rule held it back — the caller queues that
|
|
407
|
+
// copy by interaction key so it can still fire from the background.
|
|
408
|
+
function fireAttention(kind, sessionLabel, detail, isCurrent, sessionId) {
|
|
409
|
+
var d = texts();
|
|
410
|
+
var label = d.kind[kind] || d.kindDefault;
|
|
411
|
+
var content = detail || d.bodyKind[kind] || d.bodyDefault;
|
|
412
|
+
var who = sessionLabel || "";
|
|
413
|
+
var title = "DSH · " + label;
|
|
414
|
+
var body = (who ? who + d.sep : "") + content;
|
|
415
|
+
if (quietedByVisibleSession(isCurrent)) return { title: title, body: body, sessionId: sessionId || null };
|
|
416
|
+
fire("attention", title, body, isCurrent, sessionId);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
function fireDone(sessionLabel, snippet, sessionId) {
|
|
420
|
+
var d = texts();
|
|
421
|
+
var body = sessionLabel || d.doneBody;
|
|
422
|
+
if (snippet) body = body + "\n" + snippet;
|
|
423
|
+
fire("done", d.doneTitle, body, false, sessionId);
|
|
424
|
+
}
|
|
425
|
+
// Test alerts deliberately ignore the visibility rules (attentionHiddenOnly
|
|
426
|
+
// / doneHiddenOnly): the whole point of a test button is to prove the
|
|
427
|
+
// channel works while you are looking at the page. Only the master switch
|
|
428
|
+
// (and browser notification permission) can stop them. The attention test
|
|
429
|
+
// also drops its title marker again shortly after, so a test never leaves a
|
|
430
|
+
// standing "waiting on you" mark in the tab.
|
|
431
|
+
var TEST_MARK_RELEASE_MS = 6000;
|
|
432
|
+
var TEST_MARK_KEY = "test:attention";
|
|
433
|
+
function testAlert(kind) {
|
|
434
|
+
var d = texts();
|
|
435
|
+
if (kind === "attention") {
|
|
436
|
+
deliver("attention", "DSH · " + d.kind.question, d.testAttention);
|
|
437
|
+
markAttention(TEST_MARK_KEY);
|
|
438
|
+
setTimeout(function () { unmarkAttention(TEST_MARK_KEY); }, TEST_MARK_RELEASE_MS);
|
|
439
|
+
} else {
|
|
440
|
+
deliver("done", d.doneTitle, d.testDone);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ───────────────────────── state machine ─────────────────────────
|
|
445
|
+
// prevById: per-session-id baseline of what we have already reported.
|
|
446
|
+
// { running: bool, pendingKeys: string[] , doneArmed: bool, seeded: bool }
|
|
447
|
+
var prevById = Object.create(null);
|
|
448
|
+
var faceSub = null; // current session face unsubscribe
|
|
449
|
+
var currentFaceId = null;
|
|
450
|
+
var listStore = null;
|
|
451
|
+
var rootCtx = null; // client cordis context, captured at apply()
|
|
452
|
+
// uiSession pending-interaction watcher (DSH >= 0.1.2-alpha.2)
|
|
453
|
+
var uiPending = false; // true once the interaction snapshot is readable
|
|
454
|
+
var uiSub = null;
|
|
455
|
+
var uiStore = null; // the interaction store we hold a subscription on
|
|
456
|
+
var uiBindNote = "not attempted"; // last lookup outcome, read by __dshNotifyMe.debug()
|
|
457
|
+
var uiWarned = false; // one unreachable-service warning per page load
|
|
458
|
+
var uiBaseline = Object.create(null); // sessionId -> { key: string }
|
|
459
|
+
|
|
460
|
+
// One-line detail for a pending interaction. Two shapes reach this:
|
|
461
|
+
// * the pre-0.1.2 controller payload (approval/question requested frames)
|
|
462
|
+
// — flat, its text under `text`/`question`/…;
|
|
463
|
+
// * the 0.1.2+ uiSession interaction objects — class instances whose own
|
|
464
|
+
// fields are the detail (PendingApproval: toolName/reason/callId,
|
|
465
|
+
// PendingQuestion: questions[]).
|
|
466
|
+
function textFromPayload(payload) {
|
|
467
|
+
if (!payload || typeof payload !== "object") return "";
|
|
468
|
+
function clip(s) {
|
|
469
|
+
s = String(s).replace(/\s+/g, " ").trim();
|
|
470
|
+
if (!s) return "";
|
|
471
|
+
return s.length > 160 ? s.slice(0, 160) + "…" : s;
|
|
472
|
+
}
|
|
473
|
+
// scalar fields (covers the flat pre-0.1.2 payloads and PendingApproval)
|
|
474
|
+
var keys = ["text", "question", "prompt", "title", "summary", "description", "message", "operation", "toolName", "reason", "name", "tool"];
|
|
475
|
+
for (var i = 0; i < keys.length; i++) {
|
|
476
|
+
var v = payload[keys[i]];
|
|
477
|
+
if (typeof v === "string") {
|
|
478
|
+
var s = clip(v);
|
|
479
|
+
if (s) {
|
|
480
|
+
// PendingApproval carries both the tool and the human-readable
|
|
481
|
+
// reason — "pwsh" alone says nothing useful in a toast.
|
|
482
|
+
if (keys[i] === "toolName" && typeof payload.reason === "string" && clip(payload.reason)) {
|
|
483
|
+
return s + " · " + clip(payload.reason);
|
|
484
|
+
}
|
|
485
|
+
return s;
|
|
486
|
+
}
|
|
487
|
+
} else if (v && typeof v === "object" && typeof v.text === "string" && clip(v.text)) {
|
|
488
|
+
return clip(v.text);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// nested batches (PendingQuestion.questions -> { question, detail, options })
|
|
492
|
+
var qs = payload.questions;
|
|
493
|
+
if (Array.isArray(qs) && qs.length > 0) {
|
|
494
|
+
var q = qs[0];
|
|
495
|
+
if (typeof q === "string") return clip(q);
|
|
496
|
+
if (q && typeof q === "object") {
|
|
497
|
+
var qk = ["question", "title", "text", "detail"];
|
|
498
|
+
for (var j = 0; j < qk.length; j++) {
|
|
499
|
+
if (typeof q[qk[j]] === "string" && clip(q[qk[j]])) return clip(q[qk[j]]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return "";
|
|
504
|
+
}
|
|
505
|
+
function snippetOfLastAssistant(snap) {
|
|
506
|
+
try {
|
|
507
|
+
var nodes = snap.nodes || [];
|
|
508
|
+
for (var i = nodes.length - 1; i >= 0; i--) {
|
|
509
|
+
var nd = nodes[i];
|
|
510
|
+
if (nd && nd.kind === "assistant" && nd.blocks) {
|
|
511
|
+
for (var b = nd.blocks.length - 1; b >= 0; b--) {
|
|
512
|
+
if (nd.blocks[b] && nd.blocks[b].kind === "text" && nd.blocks[b].text) {
|
|
513
|
+
var t = nd.blocks[b].text.replace(/\s+/g, " ").trim();
|
|
514
|
+
if (t) return t.length > 80 ? t.slice(0, 80) + "…" : t;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch (e) {}
|
|
520
|
+
return "";
|
|
521
|
+
}
|
|
522
|
+
// Evaluate one row/face observation. row: {id, running, pending:[{key,kind,payload}], completed, label, face:bool}
|
|
523
|
+
function evaluateRow(row) {
|
|
524
|
+
var prev = prevById[row.id];
|
|
525
|
+
var runningNow = !!row.running;
|
|
526
|
+
var pendingNow = row.pending || [];
|
|
527
|
+
var prevPendingKeys = prev ? prev.pendingKeys : [];
|
|
528
|
+
var wasRunning = prev ? prev.running : false;
|
|
529
|
+
var newKeys = [];
|
|
530
|
+
for (var i = 0; i < pendingNow.length; i++) {
|
|
531
|
+
var pk = pendingNow[i].key;
|
|
532
|
+
if (prevPendingKeys.indexOf(pk) === -1 && !attentionKeysFired[pk]) {
|
|
533
|
+
newKeys.push(pendingNow[i]);
|
|
534
|
+
attentionKeysFired[pk] = true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
var hasPendingNow = pendingNow.length > 0;
|
|
538
|
+
|
|
539
|
+
// 1) a pending interaction just appeared -> the agent is waiting on the user.
|
|
540
|
+
// First evaluation of a session also lands here (prevPendingKeys empty),
|
|
541
|
+
// so a wait that already existed when the plugin (or page) mounted still
|
|
542
|
+
// alerts once.
|
|
543
|
+
// On hosts that expose uiSession this is the fallback path only: the
|
|
544
|
+
// interaction snapshot there is the authoritative source, and it drives
|
|
545
|
+
// the title marker itself (see the uiSession watcher below).
|
|
546
|
+
if (newKeys.length > 0) {
|
|
547
|
+
var it = newKeys[0];
|
|
548
|
+
var copy = fireAttention(it.kind, sessionLabelText(row), textFromPayload(it.payload), !!row.face, row.id);
|
|
549
|
+
if (copy) queueQuieted(it.key, copy);
|
|
550
|
+
lastAttentionAt = Date.now();
|
|
551
|
+
markAttention(it.key);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 2) a non-selected session finished while we were away (sidebar "done")
|
|
555
|
+
var notified = false;
|
|
556
|
+
if (!row.face && row.completed && prev && !prev.completed && prev.seeded) {
|
|
557
|
+
fireDone(sessionLabelText(row), "", row.id);
|
|
558
|
+
notified = true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 3) running true -> false on a session we were watching: a reply finished
|
|
562
|
+
if (!notified && prev && prev.seeded && wasRunning && !runningNow && !hasPendingNow) {
|
|
563
|
+
// ignore the edge if it happened in the same tick as an attention alert
|
|
564
|
+
if (Date.now() - lastAttentionAt < 300) {
|
|
565
|
+
/* answered by an attention alert above */
|
|
566
|
+
} else {
|
|
567
|
+
fireDone(sessionLabelText(row), row.face ? snippetOfLastAssistant(row.faceSnap) : "", row.id);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// store the next baseline
|
|
572
|
+
prevById[row.id] = {
|
|
573
|
+
running: runningNow,
|
|
574
|
+
pendingKeys: pendingNow.map(function (p) { return p.key; }),
|
|
575
|
+
completed: !!row.completed,
|
|
576
|
+
seeded: true
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// a wait we alerted about has been resolved -> drop the title marker
|
|
580
|
+
if (!uiPending && prev && prevPendingKeys.length > 0 && pendingNow.length === 0) {
|
|
581
|
+
for (var r = 0; r < prevPendingKeys.length; r++) unmarkAttention(prevPendingKeys[r]);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// face -> row adapter (selected session, rich data)
|
|
586
|
+
function evalSessionFace(sessionId, snap) {
|
|
587
|
+
if (sessionId !== currentFaceId) return;
|
|
588
|
+
var pend = (snap.pending || []).map(function (p) {
|
|
589
|
+
return { key: p.key || (p.kind + ":" + sessionId), kind: p.kind, payload: p.payload || null };
|
|
590
|
+
});
|
|
591
|
+
evaluateRow({
|
|
592
|
+
id: sessionId,
|
|
593
|
+
running: !!snap.running,
|
|
594
|
+
pending: pend,
|
|
595
|
+
completed: false,
|
|
596
|
+
face: true,
|
|
597
|
+
faceSnap: snap,
|
|
598
|
+
label: "current"
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
// list row -> adapter (every other listed session)
|
|
602
|
+
function evalListRow(sid, sum, currentId) {
|
|
603
|
+
evaluateRow({
|
|
604
|
+
id: sid,
|
|
605
|
+
running: !!sum.running,
|
|
606
|
+
pending: sum.pendingInteraction ? [{ key: "list:" + sid + ":" + sum.pendingInteraction, kind: sum.pendingInteraction, payload: null }] : [],
|
|
607
|
+
completed: !!sum.completed,
|
|
608
|
+
face: false,
|
|
609
|
+
label: sum.displayTitle || sum.title || sid
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ─────────────────────── subscription wiring ─────────────────────
|
|
614
|
+
function bindCurrentFace() {
|
|
615
|
+
if (!listStore) return;
|
|
616
|
+
try {
|
|
617
|
+
var st = listStore.getSnapshot();
|
|
618
|
+
var cur = st && st.current;
|
|
619
|
+
if (!cur) { detachFace(); return; }
|
|
620
|
+
if (cur === currentFaceId && faceSub) return;
|
|
621
|
+
detachFace();
|
|
622
|
+
currentFaceId = cur;
|
|
623
|
+
var b = rootCtx.sessions.binding(cur);
|
|
624
|
+
if (!b || !b.session || typeof b.session.subscribe !== "function") {
|
|
625
|
+
// binding not minted yet — the retry timer will re-check
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
faceSub = b.session.subscribe(function () {
|
|
629
|
+
try {
|
|
630
|
+
evalSessionFace(cur, b.session.getSnapshot());
|
|
631
|
+
} catch (e) {}
|
|
632
|
+
});
|
|
633
|
+
// First evaluation seeds the baseline (no false completion edges) while
|
|
634
|
+
// still alerting for interactions that were already pending on bind.
|
|
635
|
+
try {
|
|
636
|
+
evalSessionFace(cur, b.session.getSnapshot());
|
|
637
|
+
} catch (e) {}
|
|
638
|
+
} catch (e) {}
|
|
639
|
+
}
|
|
640
|
+
function detachFace() {
|
|
641
|
+
if (faceSub) { try { faceSub(); } catch (e) {} faceSub = null; }
|
|
642
|
+
currentFaceId = null;
|
|
643
|
+
}
|
|
644
|
+
function onListChanged() {
|
|
645
|
+
if (!listStore) return;
|
|
646
|
+
try {
|
|
647
|
+
var st = listStore.getSnapshot();
|
|
648
|
+
var cur = st && st.current;
|
|
649
|
+
bindCurrentFace();
|
|
650
|
+
bindUiSession();
|
|
651
|
+
var byId = (st && st.byId) || {};
|
|
652
|
+
for (var sid in byId) {
|
|
653
|
+
if (sid === cur) continue; // current handled by the face watcher
|
|
654
|
+
var sum = byId[sid];
|
|
655
|
+
if (!sum) continue;
|
|
656
|
+
evalListRow(sid, sum, cur);
|
|
657
|
+
}
|
|
658
|
+
// sessions that vanished: keep baseline (they may reappear) - no action.
|
|
659
|
+
} catch (e) {}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ─────────────── uiSession pending-interaction watcher ───────────
|
|
663
|
+
// DSH >= 0.1.2-alpha.2 moved pending interactions out of the Controller
|
|
664
|
+
// snapshots into their own store (uiSession.pendingInteractions), a
|
|
665
|
+
// Map<sessionId, interaction> whose values carry kind + key + sessionId and
|
|
666
|
+
// the domain's own detail fields. It is the only live "the agent is waiting
|
|
667
|
+
// on you" source on those hosts.
|
|
668
|
+
//
|
|
669
|
+
// The service is looked up lazily rather than declared in `inject`:
|
|
670
|
+
// a cordis `inject` entry is a hard gate (the entry never activates while
|
|
671
|
+
// the service is missing, and a never-activated entry makes the whole web
|
|
672
|
+
// boot audit fail), while hosts older than 0.1.2-alpha.2 have no uiSession
|
|
673
|
+
// at all and must keep working through the controller path above.
|
|
674
|
+
//
|
|
675
|
+
// ⚠️ Reading `ctx.uiSession` directly does NOT work here, and failing to
|
|
676
|
+
// notice that is exactly how this watcher stayed dead on every 0.1.2+ host:
|
|
677
|
+
// cordis resolves a service property only for names declared in the plugin's
|
|
678
|
+
// `inject` map and throws `cannot get property "uiSession" without inject`
|
|
679
|
+
// for every other name — sibling loader entries never leak their service
|
|
680
|
+
// into an ancestor store, so the old try/catch around that read silently
|
|
681
|
+
// produced `null` forever. ctx.get() reads the reflector store with no
|
|
682
|
+
// inject requirement, which is the only lazy lookup that actually resolves
|
|
683
|
+
// the service; ctx.reflect.get() and a plain property read stay as
|
|
684
|
+
// fallbacks for contexts that predate that API.
|
|
685
|
+
function lookupService(name) {
|
|
686
|
+
var svc = null;
|
|
687
|
+
try {
|
|
688
|
+
if (rootCtx && typeof rootCtx.get === "function") svc = rootCtx.get(name) || null;
|
|
689
|
+
} catch (e) { svc = null; }
|
|
690
|
+
if (svc) return svc;
|
|
691
|
+
try {
|
|
692
|
+
if (rootCtx && rootCtx.reflect && typeof rootCtx.reflect.get === "function") svc = rootCtx.reflect.get(name) || null;
|
|
693
|
+
} catch (e) { svc = null; }
|
|
694
|
+
if (svc) return svc;
|
|
695
|
+
try { if (rootCtx && name in rootCtx) svc = rootCtx[name] || null; } catch (e) { svc = null; }
|
|
696
|
+
return svc;
|
|
697
|
+
}
|
|
698
|
+
// Did some entry on this host ever provide the service? Separates the
|
|
699
|
+
// normal "older host has no interaction store" case (stay silent, the
|
|
700
|
+
// controller path still works there) from "provided but unreachable",
|
|
701
|
+
// which is a real defect and deserves one console warning.
|
|
702
|
+
function serviceWasProvided(name) {
|
|
703
|
+
try {
|
|
704
|
+
var props = rootCtx && rootCtx.reflect && rootCtx.reflect.props;
|
|
705
|
+
return !!(props && props[name]);
|
|
706
|
+
} catch (e) { return false; }
|
|
707
|
+
}
|
|
708
|
+
function noteUiBind(note) {
|
|
709
|
+
if (uiBindNote === note) return;
|
|
710
|
+
uiBindNote = note;
|
|
711
|
+
if (note === "bound" || uiWarned || !serviceWasProvided("uiSession")) return;
|
|
712
|
+
uiWarned = true;
|
|
713
|
+
try {
|
|
714
|
+
console.warn("[dsh-notify-me] uiSession is provided but not reachable (" + note + ") — approval alerts fall back to the legacy controller path, which stopped reporting pending interactions in DSH 0.1.2+");
|
|
715
|
+
} catch (e) {}
|
|
716
|
+
}
|
|
717
|
+
function bindUiSession() {
|
|
718
|
+
var svc = lookupService("uiSession");
|
|
719
|
+
var store = svc && svc.pendingInteractions;
|
|
720
|
+
if (!store || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
|
|
721
|
+
noteUiBind(svc ? "uiSession has no pendingInteractions store" : "uiSession not visible");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (uiSub && uiStore === store) { noteUiBind("bound"); return; }
|
|
725
|
+
// the service instance was swapped (plugin HMR): drop the stale
|
|
726
|
+
// subscription and its marks, then bind the live store
|
|
727
|
+
if (uiSub) detachUiSession();
|
|
728
|
+
uiStore = store;
|
|
729
|
+
uiPending = true; // this watcher now owns the title marker
|
|
730
|
+
uiSub = store.subscribe(function () { try { evalUiPending(); } catch (e) {} });
|
|
731
|
+
noteUiBind("bound");
|
|
732
|
+
evalUiPending(); // seed: a wait that predates this plugin still alerts once
|
|
733
|
+
}
|
|
734
|
+
function evalUiPending() {
|
|
735
|
+
var svc = lookupService("uiSession");
|
|
736
|
+
var store = svc && svc.pendingInteractions;
|
|
737
|
+
if (!store || typeof store.getSnapshot !== "function") return;
|
|
738
|
+
var snap = store.getSnapshot(); // Map<sessionId, interaction>
|
|
739
|
+
if (!snap || typeof snap.forEach !== "function") return;
|
|
740
|
+
var seen = Object.create(null);
|
|
741
|
+
snap.forEach(function (it, sid) {
|
|
742
|
+
if (!it) return;
|
|
743
|
+
seen[sid] = true;
|
|
744
|
+
var key = it.key || (it.kind + ":" + sid);
|
|
745
|
+
var prev = uiBaseline[sid];
|
|
746
|
+
// new interactions alert once; a wait that already existed on bind
|
|
747
|
+
// (no baseline yet for that session) alerts here too.
|
|
748
|
+
if (prev && prev.key === key) return;
|
|
749
|
+
uiBaseline[sid] = { key: key };
|
|
750
|
+
// a wait replaced by a newer one in the same session: the old mark goes
|
|
751
|
+
// with it, otherwise the tab would keep one stale marker per replacement
|
|
752
|
+
if (prev) unmarkAttention(prev.key);
|
|
753
|
+
if (attentionKeysFired[key]) { markAttention(key); return; } // already alerted, still needs its mark
|
|
754
|
+
attentionKeysFired[key] = true;
|
|
755
|
+
var copy = fireAttention(it.kind, labelForSessionId(sid), textFromPayload(it), currentSessionId() === sid, sid);
|
|
756
|
+
if (copy) queueQuieted(key, copy);
|
|
757
|
+
lastAttentionAt = Date.now();
|
|
758
|
+
markAttention(key);
|
|
759
|
+
});
|
|
760
|
+
for (var sid2 in uiBaseline) {
|
|
761
|
+
if (seen[sid2]) continue;
|
|
762
|
+
// interaction answered, cancelled, or its session went away
|
|
763
|
+
unmarkAttention(uiBaseline[sid2].key);
|
|
764
|
+
delete uiBaseline[sid2];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function detachUiSession() {
|
|
768
|
+
if (uiSub) { try { uiSub(); } catch (e) {} uiSub = null; }
|
|
769
|
+
uiStore = null;
|
|
770
|
+
// drop every mark this watcher holds; hosts without the interaction store
|
|
771
|
+
// go back to the controller marker path
|
|
772
|
+
for (var s in uiBaseline) unmarkAttention(uiBaseline[s].key);
|
|
773
|
+
uiBaseline = Object.create(null);
|
|
774
|
+
uiPending = false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ──────────────────────── global control API ─────────────────────
|
|
778
|
+
// Expose the control API. Merge instead of replace: a consumer (or a test
|
|
779
|
+
// harness) may have registered onEvent before the plugin applied, and that
|
|
780
|
+
// hook must survive — binding the interaction store can deliver an alert
|
|
781
|
+
// synchronously, in the middle of apply().
|
|
782
|
+
var api = window.__dshNotifyMe;
|
|
783
|
+
if (!api || typeof api !== "object") api = {};
|
|
784
|
+
try {
|
|
785
|
+
Object.defineProperty(api, "config", {
|
|
786
|
+
get: function () { return readConfig(); },
|
|
787
|
+
configurable: true
|
|
788
|
+
});
|
|
789
|
+
} catch (e) {}
|
|
790
|
+
// keep in sync with package.json "version" on every release
|
|
791
|
+
api.version = "1.1.8";
|
|
792
|
+
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
793
|
+
api.resetConfig = function () {
|
|
794
|
+
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
795
|
+
config = readConfig();
|
|
796
|
+
return config;
|
|
797
|
+
};
|
|
798
|
+
// manual test; run it after clicking anywhere so permission can be granted.
|
|
799
|
+
// Test alerts fire regardless of the page-open visibility rules.
|
|
800
|
+
api.test = function (kind) {
|
|
801
|
+
kind = kind === "attention" ? "attention" : "done";
|
|
802
|
+
unlockOnce();
|
|
803
|
+
testAlert(kind);
|
|
804
|
+
return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ")";
|
|
805
|
+
};
|
|
806
|
+
// Live diagnostics for the "why did nothing pop" case. The needs-input
|
|
807
|
+
// channel has two possible owners on current hosts (uiSession on 0.1.2+,
|
|
808
|
+
// the controller snapshots before that) and both used to fail silently, so
|
|
809
|
+
// the binding state has to be readable from the page itself.
|
|
810
|
+
api.debug = function () {
|
|
811
|
+
return {
|
|
812
|
+
version: api.version,
|
|
813
|
+
enabled: config.enabled,
|
|
814
|
+
language: effectiveLang(),
|
|
815
|
+
attentionHiddenOnly: config.attentionHiddenOnly,
|
|
816
|
+
currentHiddenOnly: config.currentHiddenOnly,
|
|
817
|
+
doneHiddenOnly: config.doneHiddenOnly,
|
|
818
|
+
toast: config.toast,
|
|
819
|
+
sound: config.sound,
|
|
820
|
+
permission: ("Notification" in window) ? Notification.permission : "n/a",
|
|
821
|
+
visibility: hidden() ? "hidden" : "visible",
|
|
822
|
+
uiSession: uiPending ? "bound" : "unbound",
|
|
823
|
+
uiSessionNote: uiBindNote,
|
|
824
|
+
sessionBaselines: Object.keys(prevById).length,
|
|
825
|
+
pendingBaselines: Object.keys(uiBaseline).length,
|
|
826
|
+
firedKeys: Object.keys(attentionKeysFired),
|
|
827
|
+
markedKeys: Object.keys(attentionKeysMarked),
|
|
828
|
+
quietedKeys: Object.keys(quietedAttention),
|
|
829
|
+
lastAttentionAt: lastAttentionAt ? new Date(lastAttentionAt).toISOString() : null
|
|
830
|
+
};
|
|
831
|
+
};
|
|
832
|
+
window.__dshNotifyMe = api;
|
|
833
|
+
|
|
834
|
+
// ────────────────── Settings page (react only, optional) ─────────
|
|
835
|
+
// Registered when the web profile provides react + slots + locale; the
|
|
836
|
+
// reminder core above never depends on any of it.
|
|
837
|
+
var NS = "dsh-notify-me";
|
|
838
|
+
var UI_zh = {
|
|
839
|
+
nav: "通知提醒",
|
|
840
|
+
blurb: "当模型停下来需要你操作(审批 / 方案待确认 / 提问),或回复在后台完成时,用系统通知、提示音和标签页标题提醒你。",
|
|
841
|
+
enable: "启用提醒",
|
|
842
|
+
enableHint: "总开关:关闭后不再弹系统通知、不播放提示音,也不再改动标签页标题。",
|
|
843
|
+
toast: "系统通知",
|
|
844
|
+
toastHint: "经浏览器弹出系统 Toast(Windows 通知中心);需要已授予通知权限。",
|
|
845
|
+
sound: "提示音",
|
|
846
|
+
soundHint: "WebAudio 合成音,「需要你」与「回复完成」使用不同音型。",
|
|
847
|
+
attentionVisible: "页面打开时也提醒「需要你」",
|
|
848
|
+
attentionVisibleHint: "默认开启:模型在等你审批 / 确认方案 / 回答问题(含提问弹窗)时,即使页面在最前也会提醒。",
|
|
849
|
+
currentQuiet: "当前对话不弹通知",
|
|
850
|
+
currentQuietHint: "默认开启:等待就发生在你正看着的这个对话里时,只留标签页标记,不弹系统通知、不响提示音,免得挡住审批卡片;页面切到后台后照常提醒。",
|
|
851
|
+
doneVisible: "页面打开时也提醒「回复完成」",
|
|
852
|
+
doneVisibleHint: "默认关闭:「完成」只在页面隐藏或切到后台时才提醒,避免打扰。",
|
|
853
|
+
autoFocus: "点击通知回到对应对话",
|
|
854
|
+
autoFocusHint: "默认开启:点系统通知会把 DSH 带到前台,并直接切到这条提醒所属的对话;关掉后点击通知不做任何事。",
|
|
855
|
+
volume: "音量",
|
|
856
|
+
language: "通知语言",
|
|
857
|
+
langAuto: "跟随界面",
|
|
858
|
+
langZh: "简体中文",
|
|
859
|
+
langEn: "English",
|
|
860
|
+
langAutoNote: "(跟随界面)",
|
|
861
|
+
current: "界面当前语言",
|
|
862
|
+
testTitle: "快速测试",
|
|
863
|
+
testHint: "用当前设置各发一条测试提醒(不受上面「页面打开时也提醒」开关限制)。",
|
|
864
|
+
testAttention: "测试「需要你」",
|
|
865
|
+
testDone: "测试「回复完成」",
|
|
866
|
+
reset: "恢复默认设置",
|
|
867
|
+
resetDone: "已恢复默认设置。",
|
|
868
|
+
sent: "已发送测试提醒 · 通知语言:",
|
|
869
|
+
permHint: "通知权限尚未授予:先在页面上点击一次并允许通知,系统通知才能弹出(提示音不受影响)。",
|
|
870
|
+
offHint: "已关闭提醒:先打开「启用提醒」再测试。"
|
|
871
|
+
};
|
|
872
|
+
var UI_en = {
|
|
873
|
+
nav: "Notify me",
|
|
874
|
+
blurb: "When the agent stops and needs your input (approval / plan review / question) or a reply finishes in the background, get a system notification, a sound, and a tab-title marker.",
|
|
875
|
+
enable: "Enable reminders",
|
|
876
|
+
enableHint: "Master switch: when off, no toasts, no sounds, and the tab title is never modified.",
|
|
877
|
+
toast: "System notifications",
|
|
878
|
+
toastHint: "Native toasts via the browser (Windows notification center); requires notification permission.",
|
|
879
|
+
sound: "Sound",
|
|
880
|
+
soundHint: "WebAudio beeps — \"needs you\" and \"reply finished\" use different patterns.",
|
|
881
|
+
attentionVisible: "Also alert \"needs you\" while the page is open",
|
|
882
|
+
attentionVisibleHint: "On by default: alert even in the foreground when the model waits for approval / plan review / a question.",
|
|
883
|
+
currentQuiet: "Stay quiet for the conversation on screen",
|
|
884
|
+
currentQuietHint: "On by default: a wait inside the conversation you are looking at keeps only the tab marker — no toast or sound landing on the approval card. Backgrounding the page puts the alert back.",
|
|
885
|
+
doneVisible: "Also alert \"reply finished\" while the page is open",
|
|
886
|
+
doneVisibleHint: "Off by default: \"done\" only alerts while the page is hidden or backgrounded, to avoid noise.",
|
|
887
|
+
autoFocus: "Click a toast to open that conversation",
|
|
888
|
+
autoFocusHint: "On by default: clicking a toast brings DSH forward and switches to the conversation the alert came from. When off, clicking a toast does nothing.",
|
|
889
|
+
volume: "Volume",
|
|
890
|
+
language: "Notification language",
|
|
891
|
+
langAuto: "Follow the interface",
|
|
892
|
+
langZh: "简体中文",
|
|
893
|
+
langEn: "English",
|
|
894
|
+
langAutoNote: " (follows the interface)",
|
|
895
|
+
current: "Current interface language",
|
|
896
|
+
testTitle: "Quick test",
|
|
897
|
+
testHint: "Send one test alert with the current settings (not limited by the \"while the page is open\" toggles above).",
|
|
898
|
+
testAttention: "Test \"needs you\"",
|
|
899
|
+
testDone: "Test \"reply finished\"",
|
|
900
|
+
reset: "Restore defaults",
|
|
901
|
+
resetDone: "Defaults restored.",
|
|
902
|
+
sent: "Test alert sent · notification language: ",
|
|
903
|
+
permHint: "Notification permission is not granted yet — click once on the page and allow notifications so toasts can appear (sound is unaffected).",
|
|
904
|
+
offHint: "Reminders are off: enable them first, then test."
|
|
905
|
+
};
|
|
906
|
+
// Small element helper to keep the hand-written page readable.
|
|
907
|
+
function h(tag, props) {
|
|
908
|
+
var args = [tag, props || null];
|
|
909
|
+
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
910
|
+
return React.createElement.apply(React, args);
|
|
911
|
+
}
|
|
912
|
+
// css classes (namespaced to avoid collisions)
|
|
913
|
+
var UI_CSS = "" +
|
|
914
|
+
".dnm-wrap{width:100%;max-width:720px;display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9))}" +
|
|
915
|
+
".dnm-blurb{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
916
|
+
".dnm-card{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.05));border-radius:10px;overflow:hidden}" +
|
|
917
|
+
".dnm-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 14px;min-height:46px;box-sizing:border-box}" +
|
|
918
|
+
".dnm-row+.dnm-row{border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35))}" +
|
|
919
|
+
".dnm-rowText{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}" +
|
|
920
|
+
".dnm-rowLabel{font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9))}" +
|
|
921
|
+
".dnm-rowHint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
922
|
+
".dnm-switch{flex:none;position:relative;width:34px;height:20px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.45));background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.2));cursor:pointer;padding:0;transition:background .15s ease}" +
|
|
923
|
+
".dnm-switch.on{background:var(--dsw-alias-state-business-primary,#3b82f6);border-color:transparent}" +
|
|
924
|
+
".dnm-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}" +
|
|
925
|
+
".dnm-switch.on .dnm-knob{left:16px}" +
|
|
926
|
+
".dnm-range{width:150px;accent-color:var(--dsw-alias-state-business-primary,#3b82f6)}" +
|
|
927
|
+
".dnm-select{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 8px;font-size:13px;font-family:inherit}" +
|
|
928
|
+
".dnm-btn{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 12px;font-size:13px;cursor:pointer;font-family:inherit}" +
|
|
929
|
+
".dnm-btn:hover{background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.1))}" +
|
|
930
|
+
".dnm-btn[disabled]{opacity:.45;cursor:default}" +
|
|
931
|
+
".dnm-btnRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}" +
|
|
932
|
+
".dnm-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
933
|
+
".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}";
|
|
934
|
+
(function injectStyle() {
|
|
935
|
+
try {
|
|
936
|
+
if (typeof document === "undefined" || typeof document.createElement !== "function") return;
|
|
937
|
+
var tagId = "dsh-notify-me/settings";
|
|
938
|
+
if (document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]")) return;
|
|
939
|
+
var s = document.createElement("style");
|
|
940
|
+
s.dataset.plugin = "dsh-notify-me";
|
|
941
|
+
s.dataset.pluginCss = tagId;
|
|
942
|
+
s.textContent = UI_CSS;
|
|
943
|
+
(document.head || document.documentElement).appendChild(s);
|
|
944
|
+
} catch (e) {}
|
|
945
|
+
})();
|
|
946
|
+
|
|
947
|
+
function rowSwitch(key, labelText, hintText, checked, onChange) {
|
|
948
|
+
return h("div", { className: "dnm-row" }, [
|
|
949
|
+
h("div", { className: "dnm-rowText" }, [
|
|
950
|
+
h("div", { className: "dnm-rowLabel" }, labelText),
|
|
951
|
+
hintText ? h("div", { className: "dnm-rowHint" }, hintText) : null
|
|
952
|
+
]),
|
|
953
|
+
h("button", {
|
|
954
|
+
type: "button",
|
|
955
|
+
role: "switch",
|
|
956
|
+
"aria-checked": checked ? "true" : "false",
|
|
957
|
+
className: "dnm-switch" + (checked ? " on" : ""),
|
|
958
|
+
onClick: function () { onChange(!checked); }
|
|
959
|
+
}, h("span", { className: "dnm-knob" }))
|
|
960
|
+
]);
|
|
961
|
+
}
|
|
962
|
+
function card(children) {
|
|
963
|
+
return h("div", { className: "dnm-card" }, children);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function NotifySettingsView(props) {
|
|
967
|
+
var ctx = props.ctx;
|
|
968
|
+
var t = props.t;
|
|
969
|
+
// Keep the page copy in sync with the active DSH UI language.
|
|
970
|
+
React.useSyncExternalStore(function (cb) { return ctx.locale.subscribe(cb); }, function () { return ctx.locale.getSnapshot(); });
|
|
971
|
+
var cfgState = React.useState(function () { return readConfig(); });
|
|
972
|
+
var cfg = cfgState[0];
|
|
973
|
+
var setCfg = cfgState[1];
|
|
974
|
+
var feedbackState = React.useState("");
|
|
975
|
+
var feedback = feedbackState[0];
|
|
976
|
+
var setFeedback = feedbackState[1];
|
|
977
|
+
function patch(p) {
|
|
978
|
+
setCfg(setConfigLocal(p));
|
|
979
|
+
}
|
|
980
|
+
function runTest(kind) {
|
|
981
|
+
if (!config.enabled) { setFeedback(t("offHint")); return; }
|
|
982
|
+
unlockOnce();
|
|
983
|
+
// Bypasses the visibility rules on purpose: a test must fire while the
|
|
984
|
+
// page is open, otherwise the button proves nothing.
|
|
985
|
+
testAlert(kind);
|
|
986
|
+
var langName = effectiveLang() === "en" ? t("langEn") : t("langZh");
|
|
987
|
+
setFeedback(t("sent") + langName);
|
|
988
|
+
}
|
|
989
|
+
var permOK = ("Notification" in window) ? Notification.permission === "granted" : false;
|
|
990
|
+
var resolvedLang = effectiveLang();
|
|
991
|
+
var resolvedLangName = resolvedLang === "en" ? t("langEn") : t("langZh");
|
|
992
|
+
return h("div", { className: "dnm-wrap" }, [
|
|
993
|
+
h("p", { className: "dnm-blurb" }, t("blurb")),
|
|
994
|
+
card([
|
|
995
|
+
rowSwitch("enabled", t("enable"), t("enableHint"), cfg.enabled, function (v) {
|
|
996
|
+
patch({ enabled: v });
|
|
997
|
+
if (!v && attentionActive > 0) { attentionActive = 0; applyTitle(); }
|
|
998
|
+
})
|
|
999
|
+
]),
|
|
1000
|
+
card([
|
|
1001
|
+
rowSwitch("toast", t("toast"), t("toastHint"), cfg.toast, function (v) { patch({ toast: v }); }),
|
|
1002
|
+
rowSwitch("sound", t("sound"), t("soundHint"), cfg.sound, function (v) { patch({ sound: v }); }),
|
|
1003
|
+
rowSwitch("attentionVisible", t("attentionVisible"), t("attentionVisibleHint"), !cfg.attentionHiddenOnly, function (v) { patch({ attentionHiddenOnly: !v }); }),
|
|
1004
|
+
rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
|
|
1005
|
+
rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
|
|
1006
|
+
rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); }),
|
|
1007
|
+
h("div", { className: "dnm-row" }, [
|
|
1008
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1009
|
+
h("div", { className: "dnm-rowLabel" }, t("volume")),
|
|
1010
|
+
h("div", { className: "dnm-rowHint" }, String(Math.round(cfg.volume * 100)) + "%")
|
|
1011
|
+
]),
|
|
1012
|
+
h("input", {
|
|
1013
|
+
type: "range",
|
|
1014
|
+
className: "dnm-range",
|
|
1015
|
+
min: 0, max: 100, step: 5,
|
|
1016
|
+
value: Math.round(cfg.volume * 100),
|
|
1017
|
+
"aria-label": t("volume"),
|
|
1018
|
+
onChange: function (e) { patch({ volume: Number(e.currentTarget.value) / 100 }); }
|
|
1019
|
+
})
|
|
1020
|
+
])
|
|
1021
|
+
]),
|
|
1022
|
+
card([
|
|
1023
|
+
h("div", { className: "dnm-row" }, [
|
|
1024
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1025
|
+
h("div", { className: "dnm-rowLabel" }, t("language")),
|
|
1026
|
+
h("div", { className: "dnm-rowHint" }, resolvedLangName + (cfg.language === "auto" ? t("langAutoNote") : ""))
|
|
1027
|
+
]),
|
|
1028
|
+
h("select", {
|
|
1029
|
+
className: "dnm-select",
|
|
1030
|
+
value: cfg.language,
|
|
1031
|
+
"aria-label": t("language"),
|
|
1032
|
+
onChange: function (e) { patch({ language: e.currentTarget.value }); }
|
|
1033
|
+
}, [
|
|
1034
|
+
h("option", { value: "auto" }, t("langAuto")),
|
|
1035
|
+
h("option", { value: "zh" }, t("langZh")),
|
|
1036
|
+
h("option", { value: "en" }, t("langEn"))
|
|
1037
|
+
])
|
|
1038
|
+
]),
|
|
1039
|
+
h("div", { className: "dnm-row" }, [
|
|
1040
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1041
|
+
h("div", { className: "dnm-rowLabel" }, t("testTitle")),
|
|
1042
|
+
h("div", { className: "dnm-rowHint" }, t("testHint"))
|
|
1043
|
+
]),
|
|
1044
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
1045
|
+
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
|
|
1046
|
+
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone"))
|
|
1047
|
+
])
|
|
1048
|
+
]),
|
|
1049
|
+
h("div", { className: "dnm-row" }, [
|
|
1050
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1051
|
+
h("div", { className: "dnm-rowLabel" }, t("reset")),
|
|
1052
|
+
h("div", { className: "dnm-rowHint" }, " ")
|
|
1053
|
+
]),
|
|
1054
|
+
h("button", { type: "button", className: "dnm-btn", onClick: function () {
|
|
1055
|
+
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
1056
|
+
config = readConfig();
|
|
1057
|
+
applyTitle();
|
|
1058
|
+
setCfg(config);
|
|
1059
|
+
setFeedback(t("resetDone"));
|
|
1060
|
+
} }, t("reset"))
|
|
1061
|
+
]),
|
|
1062
|
+
!permOK ? h("div", { className: "dnm-row" }, [
|
|
1063
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("permHint"))])
|
|
1064
|
+
]) : null
|
|
1065
|
+
]),
|
|
1066
|
+
h("p", { className: "dnm-feedback" }, feedback)
|
|
1067
|
+
]);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ────────────────────────── plugin body ──────────────────────────
|
|
1071
|
+
// Hard service requirements only. `uiSession` is deliberately NOT listed:
|
|
1072
|
+
// a cordis inject entry is a hard gate (the entry stays pending while the
|
|
1073
|
+
// service is missing, and the web boot audit fails on entries that never
|
|
1074
|
+
// activate), while hosts older than 0.1.2-alpha.2 have no uiSession at all.
|
|
1075
|
+
// It is therefore looked up lazily in bindUiSession(), with the controller
|
|
1076
|
+
// path above as the fallback. The bundle entry
|
|
1077
|
+
// "@deepseek-ai/dsh-client-ui-session" stays in package.json's
|
|
1078
|
+
// dsh.client.inject so the load order keeps the bundle in the graph.
|
|
1079
|
+
var inject = ["sessions", "locale", "slots"];
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Client plugin body: watch the sessions service and raise desktop
|
|
1083
|
+
* notifications + sounds when the agent needs input or a reply finishes;
|
|
1084
|
+
* also contributes a Settings page (settings.section slot).
|
|
1085
|
+
* @param ctx - client root context.
|
|
1086
|
+
*/
|
|
1087
|
+
function apply(ctx) {
|
|
1088
|
+
rootCtx = ctx;
|
|
1089
|
+
try {
|
|
1090
|
+
if (ctx.locale && typeof ctx.locale.getLocale === "function") localeSvc = ctx.locale;
|
|
1091
|
+
} catch (e) {}
|
|
1092
|
+
// Settings UI — only when the full web profile (react + slots + locale)
|
|
1093
|
+
// is present. The reminder core above never depends on this.
|
|
1094
|
+
try {
|
|
1095
|
+
if (React && React.createElement && ctx.slots && ctx.locale && typeof ctx.locale.register === "function" && typeof ctx.slots.inject === "function") {
|
|
1096
|
+
ctx.effect(function () {
|
|
1097
|
+
ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
|
|
1098
|
+
}, "dsh-notify-me: settings dictionaries");
|
|
1099
|
+
var t = ctx.locale.bind(NS);
|
|
1100
|
+
ctx.slots.inject("settings.section", function () {
|
|
1101
|
+
return ctx.slots.register({
|
|
1102
|
+
name: "settings.section",
|
|
1103
|
+
id: "dsh-notify-me",
|
|
1104
|
+
order: 45,
|
|
1105
|
+
label: function () { return t("nav"); },
|
|
1106
|
+
locale: NS,
|
|
1107
|
+
inject: function () { return { t: t }; }
|
|
1108
|
+
}, function (ownerProps) {
|
|
1109
|
+
return React.createElement(NotifySettingsView, { ctx: ctx, t: t });
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
try { console.error("[dsh-notify-me] settings page unavailable", e); } catch (e2) {}
|
|
1115
|
+
}
|
|
1116
|
+
ctx.effect(function () {
|
|
1117
|
+
var disposed = false;
|
|
1118
|
+
var unsubs = [];
|
|
1119
|
+
var retryTimer = null;
|
|
1120
|
+
try {
|
|
1121
|
+
var sessionsSvc = ctx.sessions;
|
|
1122
|
+
if (!sessionsSvc || !sessionsSvc.list || typeof sessionsSvc.list.subscribe !== "function") {
|
|
1123
|
+
try { console.log("[dsh-notify-me] sessions service unavailable — watchers idle"); } catch (e) {}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
listStore = sessionsSvc.list;
|
|
1127
|
+
unsubs.push(sessionsSvc.list.subscribe(onListChanged));
|
|
1128
|
+
// A wait kept quiet because it was already on screen still has to
|
|
1129
|
+
// reach the user the moment they leave the page.
|
|
1130
|
+
var onVisibility = function () { try { flushQuieted(); } catch (e) {} };
|
|
1131
|
+
try { document.addEventListener("visibilitychange", onVisibility); } catch (e) {}
|
|
1132
|
+
unsubs.push(function () { try { document.removeEventListener("visibilitychange", onVisibility); } catch (e) {} });
|
|
1133
|
+
// Interaction source first: it owns the title marker once present, so
|
|
1134
|
+
// binding it before the controller scan keeps a single mark per wait.
|
|
1135
|
+
bindUiSession();
|
|
1136
|
+
// initial scan (baseline only; no alerts for pre-existing sessions)
|
|
1137
|
+
onListChanged();
|
|
1138
|
+
// binding may resolve a tick later; re-check periodically while idle
|
|
1139
|
+
retryTimer = setInterval(function () {
|
|
1140
|
+
if (disposed) return;
|
|
1141
|
+
try { bindCurrentFace(); } catch (e) {}
|
|
1142
|
+
try { bindUiSession(); } catch (e) {}
|
|
1143
|
+
}, 1500);
|
|
1144
|
+
try { console.log("[dsh-notify-me] active — window.__dshNotifyMe 可配置提醒 (enabled=" + config.enabled + ", lang=" + effectiveLang() + ", toast=" + config.toast + ", sound=" + config.sound + ")"); } catch (e) {}
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
try { console.error("[dsh-notify-me] init failed", e); } catch (e2) {}
|
|
1147
|
+
}
|
|
1148
|
+
return function () {
|
|
1149
|
+
disposed = true;
|
|
1150
|
+
if (retryTimer) clearInterval(retryTimer);
|
|
1151
|
+
for (var i = 0; i < unsubs.length; i++) { try { unsubs[i](); } catch (e) {} }
|
|
1152
|
+
detachFace();
|
|
1153
|
+
detachUiSession();
|
|
1154
|
+
};
|
|
1155
|
+
}, "dsh-notify-me: watchers");
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
exports.inject = inject;
|
|
1159
|
+
exports.apply = apply;
|
|
1160
|
+
return module.exports;
|
|
1161
|
+
}
|
|
1162
|
+
});
|