@goodandready/dsh-goal 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,1422 +1,1528 @@
1
- window.__ModuleLoader__.load({
2
- id: '@goodandready/dsh-goal',
3
- factory: (require) => {
4
- const module = { exports: {} };
5
- const React = require('react');
6
- const { useState, useEffect, useRef, useCallback, useMemo } = React;
7
-
8
- const NS = 'dsh-goal';
9
-
10
- // Безопасная загрузка примитивов ядра
11
- let Primitives = null;
12
- try {
13
- Primitives = require('@deepseek-ai/dsh-client-ui-primitives');
14
- } catch (_) {
15
- Primitives = null;
16
- }
17
-
18
- // --- SVG ИКОНКИ ---
19
-
20
- function IconTarget({ size = 15, className = '' }) {
21
- return React.createElement(
22
- 'svg',
23
- {
24
- width: size,
25
- height: size,
26
- viewBox: '0 0 24 24',
27
- fill: 'none',
28
- stroke: 'currentColor',
29
- strokeWidth: 2,
30
- strokeLinecap: 'round',
31
- strokeLinejoin: 'round',
32
- className,
33
- },
34
- React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
35
- React.createElement('circle', { cx: 12, cy: 12, r: 6 }),
36
- React.createElement('circle', { cx: 12, cy: 12, r: 2 }),
37
- );
38
- }
39
-
40
- function IconTrash({ size = 14, className = '' }) {
41
- return React.createElement(
42
- 'svg',
43
- {
44
- width: size,
45
- height: size,
46
- viewBox: '0 0 24 24',
47
- fill: 'none',
48
- stroke: 'currentColor',
49
- strokeWidth: 1.8,
50
- strokeLinecap: 'round',
51
- strokeLinejoin: 'round',
52
- className,
53
- },
54
- React.createElement('path', { d: 'M3 6h18' }),
55
- React.createElement('path', { d: 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6' }),
56
- React.createElement('path', { d: 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2' }),
57
- );
58
- }
59
-
60
- function IconPause({ size = 14, className = '' }) {
61
- return React.createElement(
62
- 'svg',
63
- {
64
- width: size,
65
- height: size,
66
- viewBox: '0 0 24 24',
67
- fill: 'none',
68
- stroke: 'currentColor',
69
- strokeWidth: 1.8,
70
- strokeLinecap: 'round',
71
- strokeLinejoin: 'round',
72
- className,
73
- },
74
- React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
75
- React.createElement('line', { x1: 10, y1: 15, x2: 10, y2: 9 }),
76
- React.createElement('line', { x1: 14, y1: 15, x2: 14, y2: 9 }),
77
- );
78
- }
79
-
80
- function IconPlay({ size = 14, className = '' }) {
81
- return React.createElement(
82
- 'svg',
83
- {
84
- width: size,
85
- height: size,
86
- viewBox: '0 0 24 24',
87
- fill: 'none',
88
- stroke: 'currentColor',
89
- strokeWidth: 1.8,
90
- strokeLinecap: 'round',
91
- strokeLinejoin: 'round',
92
- className,
93
- },
94
- React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
95
- React.createElement('polygon', { points: '10 8 16 12 10 16 10 8', fill: 'currentColor' }),
96
- );
97
- }
98
-
99
- function IconCheck({ size = 15, className = '' }) {
100
- return React.createElement(
101
- 'svg',
102
- {
103
- width: size,
104
- height: size,
105
- viewBox: '0 0 24 24',
106
- fill: 'none',
107
- stroke: 'currentColor',
108
- strokeWidth: 2.5,
109
- strokeLinecap: 'round',
110
- strokeLinejoin: 'round',
111
- className,
112
- },
113
- React.createElement('polyline', { points: '20 6 9 17 4 12' }),
114
- );
115
- }
116
-
117
- function IconExpand({ size = 14, className = '' }) {
118
- return React.createElement(
119
- 'svg',
120
- {
121
- width: size,
122
- height: size,
123
- viewBox: '0 0 24 24',
124
- fill: 'none',
125
- stroke: 'currentColor',
126
- strokeWidth: 1.8,
127
- strokeLinecap: 'round',
128
- strokeLinejoin: 'round',
129
- className,
130
- },
131
- React.createElement('polyline', { points: '15 3 21 3 21 9' }),
132
- React.createElement('polyline', { points: '9 21 3 21 3 15' }),
133
- React.createElement('line', { x1: 21, y1: 3, x2: 14, y2: 10 }),
134
- React.createElement('line', { x1: 3, y1: 21, x2: 10, y2: 14 }),
135
- );
136
- }
137
-
138
- function IconRotateCcw({ size = 13, className = '' }) {
139
- return React.createElement(
140
- 'svg',
141
- {
142
- width: size,
143
- height: size,
144
- viewBox: '0 0 24 24',
145
- fill: 'none',
146
- stroke: 'currentColor',
147
- strokeWidth: 2,
148
- strokeLinecap: 'round',
149
- strokeLinejoin: 'round',
150
- className,
151
- },
152
- React.createElement('polyline', { points: '1 4 1 10 7 10' }),
153
- React.createElement('path', { d: 'M3.51 15a9 9 0 1 0 2.13-9.36L1 10' }),
154
- );
155
- }
156
-
157
- function IconChevronDown({ size = 14, className = '' }) {
158
- return React.createElement(
159
- 'svg',
160
- {
161
- width: size,
162
- height: size,
163
- viewBox: '0 0 24 24',
164
- fill: 'none',
165
- stroke: 'currentColor',
166
- strokeWidth: 2,
167
- strokeLinecap: 'round',
168
- strokeLinejoin: 'round',
169
- className,
170
- },
171
- React.createElement('polyline', { points: '6 9 12 15 18 9' }),
172
- );
173
- }
174
-
175
- // --- СТИЛИ И CSS ПРАВИЛА (СТАНДАРТ DSH-CLINEBOT & ДИЗАЙН-СИСТЕМА DSH) ---
176
- const CSS_STYLES = `
177
- .dsh-goal-dock {
178
- box-sizing: border-box;
179
- width: calc(100% - var(--dsh-composer-side-clearance, 0px) * 2 - var(--dsh-composer-dock-inset, 0px) * 4);
180
- max-width: calc(var(--dsh-composer-card-max-width, 768px) - var(--dsh-composer-dock-inset, 0px) * 4);
181
- margin: 0 auto 8px auto;
182
- display: flex;
183
- justify-content: center;
184
- }
185
- .dsh-goal-banner {
186
- box-sizing: border-box;
187
- width: 100%;
188
- display: flex;
189
- align-items: center;
190
- justify-content: space-between;
191
- background: var(--dsw-alias-bg-layer-3);
192
- border: 1px solid var(--dsw-alias-border-l2);
193
- border-radius: 12px;
194
- padding: 7px 14px;
195
- font-family: inherit;
196
- font-size: 13px;
197
- color: var(--dsw-alias-label-primary);
198
- user-select: none;
199
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
200
- transition: all 0.15s ease;
201
- }
202
- .dsh-goal-banner:hover {
203
- border-color: var(--dsw-alias-label-tertiary);
204
- }
205
- .dsh-goal-banner.completed {
206
- border-color: var(--dsw-alias-state-success-primary);
207
- background: var(--dsw-alias-bg-layer-3);
208
- }
209
- .dsh-goal-left {
210
- display: flex;
211
- align-items: center;
212
- gap: 8px;
213
- min-width: 0;
214
- flex: 1;
215
- }
216
- .dsh-goal-icon {
217
- color: var(--dsw-alias-label-secondary);
218
- flex-shrink: 0;
219
- display: flex;
220
- align-items: center;
221
- }
222
- .dsh-goal-badge {
223
- font-size: 11px;
224
- padding: 2px 8px;
225
- border-radius: 999px;
226
- border: 1px solid var(--dsw-alias-border-l2);
227
- display: inline-flex;
228
- align-items: center;
229
- gap: 4px;
230
- font-weight: 600;
231
- white-space: nowrap;
232
- background: var(--dsw-alias-bg-layer-2);
233
- color: var(--dsw-alias-label-primary);
234
- }
235
- .dsh-goal-badge-ok {
236
- border-color: var(--dsw-alias-state-success-primary);
237
- color: var(--dsw-alias-state-success-primary);
238
- background: rgba(16, 185, 129, 0.08);
239
- }
240
- .dsh-goal-badge-warn {
241
- border-color: var(--dsw-alias-state-warning-primary);
242
- color: var(--dsw-alias-state-warning-primary);
243
- background: rgba(245, 158, 11, 0.08);
244
- }
245
- .dsh-goal-label {
246
- font-weight: 600;
247
- color: var(--dsw-alias-label-primary);
248
- white-space: nowrap;
249
- }
250
- .dsh-goal-label.completed {
251
- color: var(--dsw-alias-state-success-primary);
252
- }
253
- .dsh-goal-title {
254
- color: var(--dsw-alias-label-secondary);
255
- overflow: hidden;
256
- text-overflow: ellipsis;
257
- white-space: nowrap;
258
- font-weight: 400;
259
- flex: 1;
260
- min-width: 0;
261
- }
262
- .dsh-goal-time {
263
- color: var(--dsw-alias-label-secondary);
264
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
265
- font-size: 12px;
266
- font-weight: 500;
267
- white-space: nowrap;
268
- display: inline-flex;
269
- align-items: center;
270
- gap: 4px;
271
- flex-shrink: 0;
272
- }
273
- .dsh-goal-right {
274
- display: flex;
275
- align-items: center;
276
- gap: 8px;
277
- margin-left: 12px;
278
- flex-shrink: 0;
279
- }
280
- .dsh-goal-btn {
281
- appearance: none;
282
- font: inherit;
283
- background: var(--dsw-alias-bg-layer-2);
284
- border: 1px solid var(--dsw-alias-border-l2);
285
- padding: 5px 8px;
286
- margin: 0;
287
- color: var(--dsw-alias-label-primary);
288
- cursor: pointer;
289
- display: inline-flex;
290
- align-items: center;
291
- justify-content: center;
292
- gap: 5px;
293
- border-radius: 8px;
294
- font-size: 12px;
295
- font-weight: 500;
296
- transition: all 0.15s ease;
297
- }
298
- .dsh-goal-btn:hover:not(:disabled) {
299
- background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
300
- border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
301
- }
302
- .dsh-goal-btn.icon-only {
303
- padding: 4px 6px;
304
- border-radius: 6px;
305
- border-color: transparent;
306
- background: transparent;
307
- color: var(--dsw-alias-label-tertiary);
308
- }
309
- .dsh-goal-btn.icon-only:hover {
310
- color: var(--dsw-alias-label-primary);
311
- background: var(--dsw-alias-bg-layer-2);
312
- border-color: var(--dsw-alias-border-l2);
313
- }
314
- .dsh-goal-btn.close {
315
- font-size: 13px;
316
- font-weight: bold;
317
- padding: 3px 6px;
318
- }
319
- .dsh-goal-btn.close:hover {
320
- color: var(--dsw-alias-state-error-primary);
321
- }
322
- .dsh-goal-btn-primary {
323
- background: var(--dsw-alias-label-primary);
324
- color: var(--dsw-alias-bg-layer-3);
325
- border-color: transparent;
326
- }
327
- .dsh-goal-btn-primary:hover:not(:disabled) {
328
- opacity: 0.88;
329
- }
330
- .dsh-goal-btn-danger {
331
- color: var(--dsw-alias-state-error-primary);
332
- border-color: rgba(239, 68, 68, 0.3);
333
- }
334
- .dsh-goal-btn-danger:hover:not(:disabled) {
335
- background: rgba(239, 68, 68, 0.12) !important;
336
- border-color: rgba(239, 68, 68, 0.5);
337
- }
338
-
339
- /* Modal Details */
340
- .dsh-goal-modal-overlay {
341
- position: fixed;
342
- inset: 0;
343
- background: rgba(0, 0, 0, 0.6);
344
- display: flex;
345
- align-items: center;
346
- justify-content: center;
347
- z-index: 10000;
348
- backdrop-filter: blur(4px);
349
- }
350
- .dsh-goal-modal {
351
- background: var(--dsw-alias-bg-layer-3);
352
- border: 1px solid var(--dsw-alias-border-l2);
353
- border-radius: 12px;
354
- width: 560px;
355
- max-width: 90vw;
356
- max-height: 80vh;
357
- display: flex;
358
- flex-direction: column;
359
- box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
360
- }
361
- .dsh-goal-modal-head {
362
- padding: 16px 20px;
363
- border-bottom: 1px solid var(--dsw-alias-border-l2);
364
- display: flex;
365
- align-items: center;
366
- justify-content: space-between;
367
- }
368
- .dsh-goal-modal-title {
369
- font-size: 16px;
370
- font-weight: 600;
371
- color: var(--dsw-alias-label-primary);
372
- display: flex;
373
- align-items: center;
374
- gap: 8px;
375
- }
376
- .dsh-goal-modal-body {
377
- padding: 18px 20px;
378
- overflow-y: auto;
379
- display: flex;
380
- flex-direction: column;
381
- gap: 14px;
382
- }
383
- .dsh-goal-stat-grid {
384
- display: grid;
385
- grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
386
- gap: 10px;
387
- }
388
- .dsh-goal-stat-box {
389
- padding: 10px 12px;
390
- border: 1px solid var(--dsw-alias-border-l2);
391
- border-radius: 8px;
392
- background: var(--dsw-alias-bg-layer-2);
393
- display: flex;
394
- flex-direction: column;
395
- gap: 2px;
396
- }
397
- .dsh-goal-stat-val {
398
- font-size: 16px;
399
- font-weight: 700;
400
- color: var(--dsw-alias-label-primary);
401
- }
402
- .dsh-goal-stat-lbl {
403
- font-size: 11px;
404
- color: var(--dsw-alias-label-secondary);
405
- }
406
- .dsh-goal-milestone-item {
407
- display: flex;
408
- align-items: flex-start;
409
- gap: 10px;
410
- padding: 10px 12px;
411
- border-radius: 8px;
412
- background: var(--dsw-alias-bg-layer-2);
413
- border: 1px solid var(--dsw-alias-border-l2);
414
- font-size: 13px;
415
- transition: all 0.15s ease;
416
- }
417
- .dsh-goal-modal-foot {
418
- padding: 14px 20px;
419
- border-top: 1px solid var(--dsw-alias-border-l2);
420
- display: flex;
421
- justify-content: flex-end;
422
- align-items: center;
423
- gap: 10px;
424
- }
425
-
426
- /* Settings Card (clinebot pattern) */
427
- .dsh-goal-card {
428
- border: 1px solid var(--dsw-alias-border-l2);
429
- background: var(--dsw-alias-bg-layer-3);
430
- border-radius: 12px;
431
- list-style: none;
432
- margin-bottom: 12px;
433
- transition: border-color 0.15s ease;
434
- }
435
- .dsh-goal-card:hover {
436
- border-color: var(--dsw-alias-label-tertiary);
437
- }
438
- .dsh-goal-card-head {
439
- appearance: none;
440
- width: 100%;
441
- font: inherit;
442
- color: inherit;
443
- text-align: left;
444
- cursor: pointer;
445
- background: transparent;
446
- border: 0;
447
- border-radius: 12px;
448
- display: flex;
449
- align-items: center;
450
- gap: 12px;
451
- padding: 16px 20px;
452
- }
453
- .dsh-goal-card-title {
454
- color: var(--dsw-alias-label-primary);
455
- font-size: 15px;
456
- font-weight: 600;
457
- line-height: 1.4;
458
- }
459
- .dsh-goal-card-sub {
460
- color: var(--dsw-alias-label-secondary);
461
- font-size: 13px;
462
- }
463
- .dsh-goal-card-body {
464
- border-top: 1px solid var(--dsw-alias-border-l2);
465
- margin: 0 20px;
466
- padding: 16px 0 16px;
467
- display: flex;
468
- flex-direction: column;
469
- gap: 14px;
470
- }
471
- .dsh-goal-card-field {
472
- display: flex;
473
- flex-direction: column;
474
- gap: 6px;
475
- }
476
- .dsh-goal-field-label-row {
477
- display: flex;
478
- align-items: center;
479
- justify-content: space-between;
480
- font-size: 13px;
481
- font-weight: 500;
482
- color: var(--dsw-alias-label-primary);
483
- }
484
- .dsh-goal-field-meta {
485
- display: flex;
486
- align-items: center;
487
- gap: 6px;
488
- font-size: 11px;
489
- color: var(--dsw-alias-label-tertiary);
490
- }
491
- .dsh-goal-override-tag {
492
- font-size: 11px;
493
- padding: 2px 6px;
494
- border-radius: 4px;
495
- background: var(--dsw-alias-bg-layer-2);
496
- color: var(--dsw-alias-label-secondary);
497
- border: 1px solid var(--dsw-alias-border-l2);
498
- }
499
- .dsh-goal-card-input {
500
- height: 36px;
501
- border: 1px solid var(--dsw-alias-border-l2);
502
- background: var(--dsw-alias-bg-layer-2);
503
- color: var(--dsw-alias-label-primary);
504
- border-radius: 8px;
505
- padding: 0 12px;
506
- font-size: 13px;
507
- width: 100%;
508
- box-sizing: border-box;
509
- }
510
- .dsh-goal-card-input:focus {
511
- outline: none;
512
- border-color: var(--dsw-alias-state-brand-primary);
513
- }
514
- .dsh-goal-card-input[aria-invalid="true"] {
515
- border-color: var(--dsw-alias-state-error-primary);
516
- }
517
- .dsh-goal-chev {
518
- margin-left: auto;
519
- flex: none;
520
- color: var(--dsw-alias-label-tertiary);
521
- transition: transform .16s ease;
522
- }
523
- .dsh-goal-chev-open {
524
- transform: rotate(180deg);
525
- }
526
- .dsh-goal-check-row {
527
- display: flex;
528
- align-items: center;
529
- justify-content: space-between;
530
- padding: 6px 0;
531
- }
532
- .dsh-goal-check {
533
- display: flex;
534
- align-items: center;
535
- gap: 8px;
536
- font-size: 13px;
537
- color: var(--dsw-alias-label-primary);
538
- cursor: pointer;
539
- }
540
- .dsh-goal-foot {
541
- border-top: 1px solid var(--dsw-alias-border-l2);
542
- display: flex;
543
- justify-content: flex-end;
544
- align-items: center;
545
- gap: 10px;
546
- padding: 14px 0 4px;
547
- }
548
- .dsh-goal-foot-note {
549
- color: var(--dsw-alias-state-error-primary);
550
- font-size: 12px;
551
- margin-right: auto;
552
- }
553
- .dsh-goal-btn-inline-reset {
554
- background: transparent;
555
- border: 0;
556
- cursor: pointer;
557
- padding: 2px 6px;
558
- display: flex;
559
- align-items: center;
560
- gap: 4px;
561
- border-radius: 4px;
562
- color: var(--dsw-alias-label-tertiary);
563
- font-size: 11px;
564
- }
565
- .dsh-goal-btn-inline-reset:hover {
566
- color: var(--dsw-alias-label-primary);
567
- background: var(--dsw-alias-bg-layer-2);
568
- }
569
- .dsh-goal-discard {
570
- appearance: none;
571
- font: inherit;
572
- cursor: pointer;
573
- border: 1px solid var(--dsw-alias-border-l2);
574
- border-radius: 8px;
575
- padding: 6px 14px;
576
- font-size: 13px;
577
- background: var(--dsw-alias-bg-layer-2);
578
- color: var(--dsw-alias-label-primary);
579
- transition: all 0.15s ease;
580
- }
581
- .dsh-goal-discard:hover:not(:disabled) {
582
- background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
583
- border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
584
- }
585
- .dsh-goal-discard:disabled {
586
- opacity: 0.4;
587
- cursor: not-allowed;
588
- }
589
- .dsh-goal-save {
590
- appearance: none;
591
- font: inherit;
592
- cursor: pointer;
593
- border: 1px solid transparent;
594
- border-radius: 8px;
595
- padding: 6px 16px;
596
- font-size: 13px;
597
- background: var(--dsw-alias-label-primary);
598
- color: var(--dsw-alias-bg-layer-3);
599
- font-weight: 500;
600
- transition: opacity 0.15s ease;
601
- }
602
- .dsh-goal-save:hover:not(:disabled) {
603
- opacity: 0.88;
604
- }
605
- .dsh-goal-save:disabled {
606
- opacity: 0.4;
607
- cursor: not-allowed;
608
- }
609
- `;
610
-
611
- function injectStylesOnce() {
612
- if (typeof document === 'undefined') return;
613
- if (document.getElementById('dsh-goal-styles')) return;
614
- const style = document.createElement('style');
615
- style.id = 'dsh-goal-styles';
616
- style.dataset.dshPlugin = NS;
617
- style.textContent = CSS_STYLES;
618
- document.head.appendChild(style);
619
- }
620
-
621
- // --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
622
- function GoalDetailsModal({ state, onClose, onAction, t }) {
623
- if (!state) return null;
624
-
625
- const milestones = state.milestones || [];
626
- const isCompleted = state.state === 'COMPLETED';
627
-
628
- return React.createElement(
629
- 'div',
630
- { className: 'dsh-goal-modal-overlay', onClick: onClose },
631
- React.createElement(
632
- 'div',
633
- {
634
- className: 'dsh-goal-modal',
635
- onClick: (e) => e.stopPropagation(),
636
- },
637
- React.createElement(
638
- 'div',
639
- { className: 'dsh-goal-modal-head' },
640
- React.createElement(
641
- 'div',
642
- { className: 'dsh-goal-modal-title' },
643
- isCompleted
644
- ? React.createElement(IconCheck, { size: 18, className: 'cb-badge-ok' })
645
- : React.createElement(IconTarget, { size: 18 }),
646
- isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
647
- ),
648
- React.createElement(
649
- 'button',
650
- {
651
- className: 'dsh-goal-btn icon-only close',
652
- onClick: onClose,
653
- title: t('close') || 'Закрыть',
654
- },
655
- '✕',
656
- ),
657
- ),
658
- React.createElement(
659
- 'div',
660
- { className: 'dsh-goal-modal-body' },
661
- React.createElement(
662
- 'div',
663
- null,
664
- React.createElement('div', { style: { fontWeight: 600, fontSize: 16, marginBottom: 4, color: 'var(--dsw-alias-label-primary)' } }, state.title),
665
- state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 6, lineHeight: 1.4 } }, state.description) : null,
666
- ),
667
- React.createElement(
668
- 'div',
669
- { className: 'dsh-goal-stat-grid' },
670
- React.createElement(
671
- 'div',
672
- { className: 'dsh-goal-stat-box' },
673
- React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedElapsed || '0s'),
674
- React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('time') || 'Время работы'),
675
- ),
676
- React.createElement(
677
- 'div',
678
- { className: 'dsh-goal-stat-box' },
679
- React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.iterationsCount || 0}/${state.maxIterations || 25}`),
680
- React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Итерации'),
681
- ),
682
- React.createElement(
683
- 'div',
684
- { className: 'dsh-goal-stat-box' },
685
- React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.progressPercent || 0}%`),
686
- React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Прогресс'),
687
- ),
688
- ),
689
- React.createElement(
690
- 'div',
691
- { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
692
- React.createElement('div', { style: { fontWeight: 600, fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('milestones') || 'План работ:'),
693
- milestones.length === 0
694
- ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
695
- : milestones.map((m, i) =>
696
- React.createElement(
697
- 'div',
698
- { key: m.id || i, className: 'dsh-goal-milestone-item' },
699
- React.createElement('span', { style: { fontSize: 15 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
700
- React.createElement(
701
- 'div',
702
- { style: { flex: 1, minWidth: 0 } },
703
- React.createElement(
704
- 'div',
705
- {
706
- style: {
707
- textDecoration: m.status === 'completed' ? 'line-through' : 'none',
708
- color: m.status === 'completed' ? 'var(--dsw-alias-label-tertiary)' : 'inherit',
709
- fontWeight: m.status === 'in_progress' ? 600 : 400,
710
- },
711
- },
712
- `${i + 1}. ${m.title}`,
713
- ),
714
- m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
715
- ),
716
- ),
717
- ),
718
- ),
719
- ),
720
- React.createElement(
721
- 'div',
722
- { className: 'dsh-goal-modal-foot' },
723
- isCompleted
724
- ? React.createElement(
725
- 'button',
726
- {
727
- className: 'dsh-goal-btn dsh-goal-btn-danger',
728
- onClick: () => {
729
- onAction('clear');
730
- onClose();
731
- },
732
- },
733
- t('closeBanner') || 'Закрыть плашку цели',
734
- )
735
- : null,
736
- React.createElement(
737
- 'button',
738
- {
739
- className: 'dsh-goal-btn dsh-goal-btn-primary',
740
- onClick: onClose,
741
- },
742
- t('close') || 'Закрыть',
743
- ),
744
- ),
745
- ),
746
- );
747
- }
748
-
749
- function sessionIdOf(ctx, props) {
750
- try {
751
- if (props?.session?.id) return String(props.session.id);
752
- if (props?.session?.sessionId) return String(props.session.sessionId);
753
- if (props?.input?.sessionId) return String(props.input.sessionId);
754
- if (ctx?.scope?.session) return String(ctx.scope.session.id || ctx.scope.session.header?.id || '');
755
- if (typeof ctx?.scope?.id === 'string') return ctx.scope.id;
756
- if (ctx?.session) return String(ctx.session.id || ctx.session.header?.id || '');
757
- if (typeof window !== 'undefined' && window.location) {
758
- const params = new URLSearchParams(window.location.search);
759
- const fromUrl = params.get('session') || params.get('sessionId');
760
- if (fromUrl) return fromUrl;
761
- }
762
- } catch (_) {}
763
- return 'default';
764
- }
765
-
766
- // --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
767
- function GoalTopBanner(props) {
768
- const { ctx } = props || {};
769
- const sid = sessionIdOf(ctx, props);
770
- const [state, setState] = useState(null);
771
- const [isModalOpen, setIsModalOpen] = useState(false);
772
- const [now, setNow] = useState(() => Date.now());
773
- const stateRef = useRef(null);
774
- stateRef.current = state;
775
-
776
- const t = (key) => {
777
- if (ctx?.locale?.getSnapshot) {
778
- const snap = ctx.locale.getSnapshot();
779
- const active = snap?.active || 'ru';
780
- return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
781
- }
782
- return LOCALES.ru[key] || LOCALES.en[key] || key;
783
- };
784
-
785
- useEffect(() => {
786
- injectStylesOnce();
787
- }, []);
788
-
789
- // Локальный секундный интервал для плавного и бесперебойного тикания таймера
790
- useEffect(() => {
791
- const ticker = setInterval(() => {
792
- setNow(Date.now());
793
- }, 1000);
794
- return () => clearInterval(ticker);
795
- }, []);
796
-
797
- const fetchState = useCallback(async () => {
798
- try {
799
- const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
800
- const res = await fetch(`/dsh-goal/state${query}`, {
801
- headers: { 'x-dsh-session-id': sid },
802
- });
803
- if (res.ok) {
804
- const data = await res.json();
805
- setState(data);
806
- }
807
- } catch (_) {}
808
- }, [sid]);
809
-
810
- // Адаптивный поллинг: 1.5s когда RUNNING и вкладка активна, 8s когда IDLE/PAUSED/COMPLETED или вкладка скрыта
811
- useEffect(() => {
812
- let timer = null;
813
- let isDisposed = false;
814
-
815
- const scheduleNextPoll = () => {
816
- if (isDisposed) return;
817
- const currentState = stateRef.current;
818
- const isDocHidden = typeof document !== 'undefined' && document.hidden;
819
- const isRunning = currentState?.hasActiveGoal && currentState?.state === 'RUNNING';
820
-
821
- let delay = 1500;
822
- if (isDocHidden) {
823
- delay = 10000;
824
- } else if (!isRunning) {
825
- delay = 8000;
826
- }
827
-
828
- timer = setTimeout(async () => {
829
- await fetchState();
830
- scheduleNextPoll();
831
- }, delay);
832
- };
833
-
834
- fetchState().then(() => {
835
- scheduleNextPoll();
836
- });
837
-
838
- const onVisibilityChange = () => {
839
- if (typeof document !== 'undefined' && !document.hidden) {
840
- if (timer) clearTimeout(timer);
841
- fetchState().then(() => {
842
- scheduleNextPoll();
843
- });
844
- }
845
- };
846
-
847
- if (typeof document !== 'undefined' && document.addEventListener) {
848
- document.addEventListener('visibilitychange', onVisibilityChange);
849
- }
850
-
851
- return () => {
852
- isDisposed = true;
853
- if (timer) clearTimeout(timer);
854
- if (typeof document !== 'undefined' && document.removeEventListener) {
855
- document.removeEventListener('visibilitychange', onVisibilityChange);
856
- }
857
- };
858
- }, [fetchState]);
859
-
860
- const handleAction = async (action, extra = {}) => {
861
- try {
862
- const res = await fetch('/dsh-goal/action', {
863
- method: 'POST',
864
- headers: {
865
- 'Content-Type': 'application/json',
866
- 'x-dsh-session-id': sid,
867
- },
868
- body: JSON.stringify({ action, sessionId: sid, ...extra }),
869
- });
870
- if (res.ok) {
871
- const data = await res.json();
872
- if (data.state) setState(data.state);
873
- }
874
- } catch (_) {}
875
- };
876
-
877
- if (!state || !state.hasActiveGoal) {
878
- return null;
879
- }
880
-
881
- const isPaused = state.state === 'PAUSED';
882
- const isCompleted = state.state === 'COMPLETED';
883
-
884
- const formatLiveElapsed = () => {
885
- if (!state) return '0s';
886
- if (!state.startedAt) return state.formattedElapsed || '0s';
887
- const endTime = state.completedAt || (state.pausedAt || now);
888
- const elapsedSec = Math.max(0, Math.floor((endTime - state.startedAt - (state.totalPausedDurationMs || 0)) / 1000));
889
- return formatElapsed(elapsedSec);
890
- };
891
-
892
- const liveElapsedStr = formatLiveElapsed();
893
-
894
- return React.createElement(
895
- React.Fragment,
896
- null,
897
- React.createElement(
898
- 'div',
899
- { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
900
- React.createElement(
901
- 'div',
902
- { className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
903
- React.createElement(
904
- 'div',
905
- { className: 'dsh-goal-left' },
906
- React.createElement(
907
- 'span',
908
- { className: 'dsh-goal-icon' },
909
- isCompleted ? React.createElement(IconCheck, { size: 16 }) : React.createElement(IconTarget, { size: 15 }),
910
- ),
911
- React.createElement(
912
- 'span',
913
- {
914
- className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isPaused ? 'dsh-goal-badge-warn' : ''}`,
915
- },
916
- isCompleted ? (t('goalCompleted') || 'Цель выполнена') : isPaused ? (t('pause') || 'На паузе') : (t('goalLabel') || 'Текущая цель'),
917
- ),
918
- React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
919
- ),
920
- React.createElement(
921
- 'div',
922
- { className: 'dsh-goal-right' },
923
- React.createElement(
924
- 'span',
925
- { className: 'dsh-goal-time' },
926
- `⏱ ${liveElapsedStr}`,
927
- ),
928
- !isCompleted
929
- ? React.createElement(
930
- 'button',
931
- {
932
- className: 'dsh-goal-btn icon-only',
933
- title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
934
- onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
935
- },
936
- isPaused ? React.createElement(IconPlay, { size: 14 }) : React.createElement(IconPause, { size: 14 }),
937
- )
938
- : null,
939
- React.createElement(
940
- 'button',
941
- {
942
- className: 'dsh-goal-btn icon-only',
943
- title: t('details') || 'Развернуть детали цели',
944
- onClick: () => setIsModalOpen(true),
945
- },
946
- React.createElement(IconExpand, { size: 14 }),
947
- ),
948
- isCompleted
949
- ? React.createElement(
950
- 'button',
951
- {
952
- className: 'dsh-goal-btn icon-only close',
953
- title: t('closeBanner') || 'Закрыть плашку цели',
954
- onClick: () => handleAction('clear'),
955
- },
956
- '✕',
957
- )
958
- : null,
959
- ),
960
- ),
961
- ),
962
- isModalOpen
963
- ? React.createElement(GoalDetailsModal, {
964
- state,
965
- onClose: () => setIsModalOpen(false),
966
- onAction: handleAction,
967
- t,
968
- })
969
- : null,
970
- );
971
- }
972
-
973
- // --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
974
- const DEFAULT_SETTINGS = {
975
- maxIterations: 25,
976
- autoDrive: true,
977
- enableSound: true,
978
- };
979
-
980
- function parseNumberField(raw) {
981
- if (raw === '' || raw === null || raw === undefined) {
982
- return undefined;
983
- }
984
- const n = Number(raw);
985
- if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
986
- return NaN;
987
- }
988
- return n;
989
- }
990
-
991
- function getFieldStatus(field, draftValue, snap) {
992
- const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
993
- const userVal = snap?.user?.[field];
994
- const effectiveVal = snap?.value?.[field] ?? baseVal;
995
-
996
- const isOverridden = userVal !== undefined && userVal !== null;
997
- const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
998
-
999
- let invalid = false;
1000
- let isDirty = false;
1001
-
1002
- if (field === 'maxIterations') {
1003
- if (draftValue !== undefined) {
1004
- if (draftValue === '') {
1005
- invalid = false;
1006
- isDirty = userVal !== undefined;
1007
- } else {
1008
- const parsed = parseNumberField(draftValue);
1009
- if (Number.isNaN(parsed)) {
1010
- invalid = true;
1011
- isDirty = true;
1012
- } else {
1013
- isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
1014
- }
1015
- }
1016
- }
1017
- } else {
1018
- if (draftValue !== undefined) {
1019
- isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
1020
- }
1021
- }
1022
-
1023
- return {
1024
- value: currentVal,
1025
- baseValue: baseVal,
1026
- userValue: userVal,
1027
- isOverridden,
1028
- isDirty,
1029
- invalid,
1030
- };
1031
- }
1032
-
1033
- function computeSavePlan(draft, snap) {
1034
- if (!draft) return [];
1035
-
1036
- const fields = ['maxIterations', 'autoDrive', 'enableSound'];
1037
- const writes = [];
1038
-
1039
- for (const f of fields) {
1040
- if (draft[f] === undefined) continue;
1041
-
1042
- const status = getFieldStatus(f, draft[f], snap);
1043
- if (status.invalid) continue;
1044
-
1045
- if (status.isDirty) {
1046
- if (f === 'maxIterations' && draft[f] === '') {
1047
- writes.push([f, undefined]);
1048
- } else if (f === 'maxIterations') {
1049
- writes.push([f, parseNumberField(draft[f])]);
1050
- } else {
1051
- writes.push([f, Boolean(draft[f])]);
1052
- }
1053
- }
1054
- }
1055
-
1056
- return writes;
1057
- }
1058
-
1059
- // --- КАРТОЧКА НАСТРОЕК (SETTINGS CARD) ---
1060
- function GoalSettingsCard(props) {
1061
- const { scope, ctx } = props || {};
1062
- const [isOpen, setIsOpen] = useState(false);
1063
- const [draft, setDraft] = useState(null);
1064
- const [saving, setSaving] = useState(false);
1065
- const [failed, setFailed] = useState(false);
1066
-
1067
- const [snap, setSnap] = useState(() => (scope ? scope.getSnapshot() : { value: DEFAULT_SETTINGS, base: DEFAULT_SETTINGS, user: {} }));
1068
-
1069
- useEffect(() => {
1070
- injectStylesOnce();
1071
- }, []);
1072
-
1073
- useEffect(() => {
1074
- if (!scope || typeof scope.subscribe !== 'function') return;
1075
- return scope.subscribe((next) => {
1076
- setSnap(next || scope.getSnapshot());
1077
- });
1078
- }, [scope]);
1079
-
1080
- const t = (key) => {
1081
- if (ctx?.locale?.getSnapshot) {
1082
- const snapLoc = ctx.locale.getSnapshot();
1083
- const active = snapLoc?.active || 'ru';
1084
- return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
1085
- }
1086
- return LOCALES.ru[key] || LOCALES.en[key] || key;
1087
- };
1088
-
1089
- const status = (snap && snap.status) || 'ready';
1090
- const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
1091
- const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
1092
- const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
1093
-
1094
- const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
1095
- const invalid = maxIterStatus.invalid;
1096
- const disabled = !scope || saving || snap?.writable === false;
1097
-
1098
- const edit = (field, val) => {
1099
- setFailed(false);
1100
- setDraft((prev) => ({
1101
- maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
1102
- autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
1103
- enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
1104
- [field]: val,
1105
- }));
1106
- };
1107
-
1108
- const resetFieldToDefault = (field) => {
1109
- if (field === 'maxIterations') {
1110
- edit(field, '');
1111
- } else {
1112
- edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
1113
- }
1114
- };
1115
-
1116
- const save = async () => {
1117
- if (!scope || !dirty || invalid) return;
1118
- setSaving(true);
1119
- setFailed(false);
1120
-
1121
- const writes = computeSavePlan(draft, snap);
1122
- let landed = true;
1123
-
1124
- for (const [field, fieldValue] of writes) {
1125
- try {
1126
- await scope.set(field, fieldValue);
1127
- } catch (err) {
1128
- console.error('[dsh-goal] failed to save setting', field, err);
1129
- landed = false;
1130
- }
1131
- }
1132
-
1133
- const fresh = scope.getSnapshot();
1134
- const user = fresh.user || {};
1135
- for (const [field, fieldValue] of writes) {
1136
- if (fieldValue === undefined) {
1137
- if (user[field] !== undefined) landed = false;
1138
- } else {
1139
- if (user[field] !== fieldValue) landed = false;
1140
- }
1141
- }
1142
-
1143
- setSnap(fresh);
1144
- setSaving(false);
1145
- if (landed) {
1146
- setDraft(null);
1147
- } else {
1148
- setFailed(true);
1149
- }
1150
- };
1151
-
1152
- const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
1153
-
1154
- return React.createElement(
1155
- 'li',
1156
- { className: 'dsh-goal-card' },
1157
- React.createElement(
1158
- 'button',
1159
- {
1160
- className: 'dsh-goal-card-head',
1161
- onClick: () => setIsOpen(!isOpen),
1162
- 'aria-expanded': isOpen,
1163
- },
1164
- React.createElement(
1165
- 'div',
1166
- { style: { flex: 1 } },
1167
- React.createElement('div', { className: 'dsh-goal-card-title' }, t('pluginTitle') || 'Цели и автономный режим (Goal Mode)'),
1168
- React.createElement('div', { className: 'dsh-goal-card-sub' }, t('pluginDesc') || 'Верхняя закреплённая панель цели, декомпозиция вех и авто-драйв'),
1169
- ),
1170
- React.createElement(Chevron, { className: `dsh-goal-chev ${isOpen ? 'dsh-goal-chev-open' : ''}` }),
1171
- ),
1172
- isOpen
1173
- ? React.createElement(
1174
- 'div',
1175
- { className: 'dsh-goal-card-body' },
1176
- status === 'loading'
1177
- ? React.createElement('p', { className: 'dsh-goal-field-hint', style: { padding: '12px 0' } }, t('loading') || 'Загрузка настроек…')
1178
- : status !== 'ready'
1179
- ? React.createElement('p', { className: 'dsh-goal-foot-note', style: { padding: '12px 0' } }, t('unavailable') || 'Настройки недоступны')
1180
- : React.createElement(
1181
- React.Fragment,
1182
- null,
1183
- React.createElement(
1184
- 'div',
1185
- { className: 'dsh-goal-card-field' },
1186
- React.createElement(
1187
- 'div',
1188
- { className: 'dsh-goal-field-label-row' },
1189
- React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1190
- React.createElement(
1191
- 'div',
1192
- { className: 'dsh-goal-field-meta' },
1193
- maxIterStatus.isOverridden
1194
- ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1195
- : null,
1196
- maxIterStatus.isOverridden
1197
- ? React.createElement(
1198
- 'button',
1199
- {
1200
- type: 'button',
1201
- className: 'dsh-goal-btn-inline-reset',
1202
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1203
- onClick: () => resetFieldToDefault('maxIterations'),
1204
- },
1205
- React.createElement(IconRotateCcw, { size: 12 }),
1206
- t('resetField') || 'По умолчанию',
1207
- )
1208
- : null,
1209
- ),
1210
- ),
1211
- React.createElement('input', {
1212
- id: 'dsh-goal-max-iter',
1213
- type: 'number',
1214
- min: 1,
1215
- placeholder: String(maxIterStatus.baseValue),
1216
- className: 'dsh-goal-card-input',
1217
- value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1218
- disabled,
1219
- 'aria-invalid': invalid,
1220
- onChange: (e) => edit('maxIterations', e.target.value),
1221
- }),
1222
- ),
1223
- React.createElement(
1224
- 'div',
1225
- { className: 'dsh-goal-check-row' },
1226
- React.createElement(
1227
- 'label',
1228
- { className: 'dsh-goal-check' },
1229
- React.createElement('input', {
1230
- type: 'checkbox',
1231
- checked: autoDriveStatus.value === true,
1232
- disabled,
1233
- onChange: (e) => edit('autoDrive', e.target.checked),
1234
- }),
1235
- t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1236
- ),
1237
- autoDriveStatus.isOverridden
1238
- ? React.createElement(
1239
- 'button',
1240
- {
1241
- type: 'button',
1242
- className: 'dsh-goal-btn-inline-reset',
1243
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1244
- onClick: () => resetFieldToDefault('autoDrive'),
1245
- },
1246
- React.createElement(IconRotateCcw, { size: 12 }),
1247
- )
1248
- : null,
1249
- ),
1250
- React.createElement(
1251
- 'div',
1252
- { className: 'dsh-goal-check-row' },
1253
- React.createElement(
1254
- 'label',
1255
- { className: 'dsh-goal-check' },
1256
- React.createElement('input', {
1257
- type: 'checkbox',
1258
- checked: enableSoundStatus.value === true,
1259
- disabled,
1260
- onChange: (e) => edit('enableSound', e.target.checked),
1261
- }),
1262
- t('soundLabel') || 'Звук по завершении цели',
1263
- ),
1264
- enableSoundStatus.isOverridden
1265
- ? React.createElement(
1266
- 'button',
1267
- {
1268
- type: 'button',
1269
- className: 'dsh-goal-btn-inline-reset',
1270
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1271
- onClick: () => resetFieldToDefault('enableSound'),
1272
- },
1273
- React.createElement(IconRotateCcw, { size: 12 }),
1274
- )
1275
- : null,
1276
- ),
1277
- React.createElement(
1278
- 'div',
1279
- { className: 'dsh-goal-foot' },
1280
- failed
1281
- ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1282
- : null,
1283
- React.createElement(
1284
- 'button',
1285
- {
1286
- className: 'dsh-goal-discard',
1287
- disabled: !dirty || disabled,
1288
- onClick: () => {
1289
- setFailed(false);
1290
- setDraft(null);
1291
- },
1292
- },
1293
- t('discard') || 'Отменить правки',
1294
- ),
1295
- React.createElement(
1296
- 'button',
1297
- { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1298
- saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1299
- ),
1300
- ),
1301
- ),
1302
- )
1303
- : null,
1304
- );
1305
- }
1306
-
1307
- // --- СЛОВАРИ ЛОКАЛИЗАЦИИ ---
1308
- const LOCALES = {
1309
- ru: {
1310
- goalLabel: 'Текущая цель',
1311
- goalCompleted: 'Цель выполнена',
1312
- clearGoal: 'Очистить цель',
1313
- closeBanner: 'Закрыть плашку цели',
1314
- pause: 'Приостановить',
1315
- resume: 'Возобновить',
1316
- details: 'Развернуть детали цели',
1317
- modalTitle: 'План и статус цели',
1318
- modalTitleCompleted: 'Цель выполнена: план и результаты',
1319
- time: 'Время работы',
1320
- milestones: 'План работ:',
1321
- noMilestones: 'Агент формирует план работ...',
1322
- close: 'Закрыть',
1323
- pluginTitle: 'Цели и автономный режим (Goal Mode)',
1324
- pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
1325
- maxIterLabel: 'Максимум итераций (Safety Limit):',
1326
- autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
1327
- soundLabel: 'Звук по завершении цели',
1328
- save: 'Сохранить',
1329
- saving: 'Сохранение…',
1330
- discard: 'Отменить правки',
1331
- resetField: 'По умолчанию',
1332
- overridden: 'изменено',
1333
- saveFailed: 'Не сохранено — исправьте и повторите',
1334
- loading: 'Загрузка настроек…',
1335
- unavailable: 'Настройки недоступны',
1336
- },
1337
- en: {
1338
- goalLabel: 'Current goal',
1339
- goalCompleted: 'Goal completed',
1340
- clearGoal: 'Clear goal',
1341
- closeBanner: 'Close goal banner',
1342
- pause: 'Pause',
1343
- resume: 'Resume',
1344
- details: 'Expand Goal Details',
1345
- modalTitle: 'Goal Plan & Status',
1346
- modalTitleCompleted: 'Goal Completed: Plan & Results',
1347
- time: 'Running time',
1348
- milestones: 'Plan of work:',
1349
- noMilestones: 'Agent is preparing the plan of work...',
1350
- close: 'Close',
1351
- pluginTitle: 'Goal Mode & Autonomous Loop',
1352
- pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
1353
- maxIterLabel: 'Max iterations (Safety Limit):',
1354
- autoDriveLabel: 'Auto-drive: keep the loop running automatically',
1355
- soundLabel: 'Sound when a goal completes',
1356
- save: 'Save',
1357
- saving: 'Saving…',
1358
- discard: 'Discard changes',
1359
- resetField: 'Default',
1360
- overridden: 'overridden',
1361
- saveFailed: 'Not saved — fix the values and try again',
1362
- loading: 'Loading settings…',
1363
- unavailable: 'Settings unavailable',
1364
- },
1365
- };
1366
-
1367
- module.exports.inject = ['slots', 'locale', 'settingsScope'];
1368
- module.exports.apply = function apply(ctx) {
1369
- // Регистрация локализации
1370
- try {
1371
- ctx.locale?.register?.(NS, LOCALES);
1372
- } catch (_) {}
1373
-
1374
- // Пространство настроек плагина
1375
- const settingsScope =
1376
- ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
1377
- ? ctx.settingsScope.bind({ namespace: NS })
1378
- : null;
1379
-
1380
- const registerSlotSafe = (name, entry, comp) => {
1381
- try {
1382
- if (typeof ctx.slots?.inject === 'function') {
1383
- ctx.slots.inject(name, () => {
1384
- try {
1385
- return ctx.slots.register(entry, comp);
1386
- } catch (_) {}
1387
- });
1388
- } else if (typeof ctx.slots?.register === 'function') {
1389
- ctx.slots.register(entry, comp);
1390
- }
1391
- } catch (_) {}
1392
- };
1393
-
1394
- // 1. Регистрация виджета цели над полем ввода (composer dock)
1395
- registerSlotSafe(
1396
- 'conversation.input.dock',
1397
- {
1398
- name: 'conversation.input.dock',
1399
- id: '@goodandready/dsh-goal',
1400
- order: 10,
1401
- locale: NS,
1402
- inject: (slotProps) => ({ ctx, ...slotProps }),
1403
- },
1404
- GoalTopBanner,
1405
- );
1406
-
1407
- // 2. Регистрация карточки настроек
1408
- registerSlotSafe(
1409
- 'settings.plugin.item',
1410
- {
1411
- name: 'settings.plugin.item',
1412
- key: NS,
1413
- locale: NS,
1414
- inject: () => ({ ctx, scope: settingsScope }),
1415
- },
1416
- GoalSettingsCard,
1417
- );
1418
- };
1419
-
1420
- return module.exports;
1421
- },
1422
- });
1
+ window.__ModuleLoader__.load({
2
+ id: '@goodandready/dsh-goal',
3
+ factory: (require) => {
4
+ const module = { exports: {} };
5
+ const React = require('react');
6
+ const { useState, useEffect, useRef, useCallback, useMemo } = React;
7
+
8
+ const NS = 'dsh-goal';
9
+
10
+ // Безопасная загрузка примитивов ядра
11
+ let Primitives = null;
12
+ try {
13
+ Primitives = require('@deepseek-ai/dsh-client-ui-primitives');
14
+ } catch (_) {
15
+ Primitives = null;
16
+ }
17
+
18
+ // --- SVG ИКОНКИ ---
19
+
20
+ function IconTarget({ size = 15, className = '' }) {
21
+ return React.createElement(
22
+ 'svg',
23
+ {
24
+ width: size,
25
+ height: size,
26
+ viewBox: '0 0 24 24',
27
+ fill: 'none',
28
+ stroke: 'currentColor',
29
+ strokeWidth: 2,
30
+ strokeLinecap: 'round',
31
+ strokeLinejoin: 'round',
32
+ className,
33
+ },
34
+ React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
35
+ React.createElement('circle', { cx: 12, cy: 12, r: 6 }),
36
+ React.createElement('circle', { cx: 12, cy: 12, r: 2 }),
37
+ );
38
+ }
39
+
40
+ function IconTrash({ size = 14, className = '' }) {
41
+ return React.createElement(
42
+ 'svg',
43
+ {
44
+ width: size,
45
+ height: size,
46
+ viewBox: '0 0 24 24',
47
+ fill: 'none',
48
+ stroke: 'currentColor',
49
+ strokeWidth: 1.8,
50
+ strokeLinecap: 'round',
51
+ strokeLinejoin: 'round',
52
+ className,
53
+ },
54
+ React.createElement('path', { d: 'M3 6h18' }),
55
+ React.createElement('path', { d: 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6' }),
56
+ React.createElement('path', { d: 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2' }),
57
+ );
58
+ }
59
+
60
+ function IconPause({ size = 14, className = '' }) {
61
+ return React.createElement(
62
+ 'svg',
63
+ {
64
+ width: size,
65
+ height: size,
66
+ viewBox: '0 0 24 24',
67
+ fill: 'none',
68
+ stroke: 'currentColor',
69
+ strokeWidth: 1.8,
70
+ strokeLinecap: 'round',
71
+ strokeLinejoin: 'round',
72
+ className,
73
+ },
74
+ React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
75
+ React.createElement('line', { x1: 10, y1: 15, x2: 10, y2: 9 }),
76
+ React.createElement('line', { x1: 14, y1: 15, x2: 14, y2: 9 }),
77
+ );
78
+ }
79
+
80
+ function IconPlay({ size = 14, className = '' }) {
81
+ return React.createElement(
82
+ 'svg',
83
+ {
84
+ width: size,
85
+ height: size,
86
+ viewBox: '0 0 24 24',
87
+ fill: 'none',
88
+ stroke: 'currentColor',
89
+ strokeWidth: 1.8,
90
+ strokeLinecap: 'round',
91
+ strokeLinejoin: 'round',
92
+ className,
93
+ },
94
+ React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
95
+ React.createElement('polygon', { points: '10 8 16 12 10 16 10 8', fill: 'currentColor' }),
96
+ );
97
+ }
98
+
99
+ function IconCheck({ size = 15, className = '' }) {
100
+ return React.createElement(
101
+ 'svg',
102
+ {
103
+ width: size,
104
+ height: size,
105
+ viewBox: '0 0 24 24',
106
+ fill: 'none',
107
+ stroke: 'currentColor',
108
+ strokeWidth: 2.5,
109
+ strokeLinecap: 'round',
110
+ strokeLinejoin: 'round',
111
+ className,
112
+ },
113
+ React.createElement('polyline', { points: '20 6 9 17 4 12' }),
114
+ );
115
+ }
116
+
117
+ function IconExpand({ size = 14, className = '' }) {
118
+ return React.createElement(
119
+ 'svg',
120
+ {
121
+ width: size,
122
+ height: size,
123
+ viewBox: '0 0 24 24',
124
+ fill: 'none',
125
+ stroke: 'currentColor',
126
+ strokeWidth: 1.8,
127
+ strokeLinecap: 'round',
128
+ strokeLinejoin: 'round',
129
+ className,
130
+ },
131
+ React.createElement('polyline', { points: '15 3 21 3 21 9' }),
132
+ React.createElement('polyline', { points: '9 21 3 21 3 15' }),
133
+ React.createElement('line', { x1: 21, y1: 3, x2: 14, y2: 10 }),
134
+ React.createElement('line', { x1: 3, y1: 21, x2: 10, y2: 14 }),
135
+ );
136
+ }
137
+
138
+ function IconRotateCcw({ size = 13, className = '' }) {
139
+ return React.createElement(
140
+ 'svg',
141
+ {
142
+ width: size,
143
+ height: size,
144
+ viewBox: '0 0 24 24',
145
+ fill: 'none',
146
+ stroke: 'currentColor',
147
+ strokeWidth: 2,
148
+ strokeLinecap: 'round',
149
+ strokeLinejoin: 'round',
150
+ className,
151
+ },
152
+ React.createElement('polyline', { points: '1 4 1 10 7 10' }),
153
+ React.createElement('path', { d: 'M3.51 15a9 9 0 1 0 2.13-9.36L1 10' }),
154
+ );
155
+ }
156
+
157
+ function IconChevronDown({ size = 14, className = '' }) {
158
+ return React.createElement(
159
+ 'svg',
160
+ {
161
+ width: size,
162
+ height: size,
163
+ viewBox: '0 0 24 24',
164
+ fill: 'none',
165
+ stroke: 'currentColor',
166
+ strokeWidth: 2,
167
+ strokeLinecap: 'round',
168
+ strokeLinejoin: 'round',
169
+ className,
170
+ },
171
+ React.createElement('polyline', { points: '6 9 12 15 18 9' }),
172
+ );
173
+ }
174
+
175
+ // --- СТИЛИ И CSS ПРАВИЛА (СТАНДАРТ DSH-CLINEBOT & ДИЗАЙН-СИСТЕМА DSH) ---
176
+ const CSS_STYLES = `
177
+ .dsh-goal-dock {
178
+ box-sizing: border-box;
179
+ width: calc(100% - var(--dsh-composer-side-clearance, 0px) * 2 - var(--dsh-composer-dock-inset, 0px) * 4);
180
+ max-width: calc(var(--dsh-composer-card-max-width, 768px) - var(--dsh-composer-dock-inset, 0px) * 4);
181
+ margin: 0 auto 8px auto;
182
+ display: flex;
183
+ justify-content: center;
184
+ }
185
+ .dsh-goal-banner {
186
+ box-sizing: border-box;
187
+ width: 100%;
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ background: var(--dsw-alias-bg-layer-3);
192
+ border: 1px solid var(--dsw-alias-border-l2);
193
+ border-radius: 12px;
194
+ padding: 7px 14px;
195
+ font-family: inherit;
196
+ font-size: 13px;
197
+ color: var(--dsw-alias-label-primary);
198
+ user-select: none;
199
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
200
+ transition: all 0.15s ease;
201
+ }
202
+ .dsh-goal-banner:hover {
203
+ border-color: var(--dsw-alias-label-tertiary);
204
+ }
205
+ .dsh-goal-banner.completed {
206
+ border-color: var(--dsw-alias-state-success-primary);
207
+ background: var(--dsw-alias-bg-layer-3);
208
+ }
209
+ .dsh-goal-left {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 8px;
213
+ min-width: 0;
214
+ flex: 1;
215
+ }
216
+ .dsh-goal-icon {
217
+ color: var(--dsw-alias-label-secondary);
218
+ flex-shrink: 0;
219
+ display: flex;
220
+ align-items: center;
221
+ }
222
+ .dsh-goal-badge {
223
+ font-size: 11px;
224
+ padding: 2px 8px;
225
+ border-radius: 999px;
226
+ border: 1px solid var(--dsw-alias-border-l2);
227
+ display: inline-flex;
228
+ align-items: center;
229
+ gap: 4px;
230
+ font-weight: 600;
231
+ white-space: nowrap;
232
+ background: var(--dsw-alias-bg-layer-2);
233
+ color: var(--dsw-alias-label-primary);
234
+ }
235
+ .dsh-goal-badge-ok {
236
+ border-color: var(--dsw-alias-state-success-primary);
237
+ color: var(--dsw-alias-state-success-primary);
238
+ background: rgba(16, 185, 129, 0.08);
239
+ }
240
+ .dsh-goal-badge-warn {
241
+ border-color: var(--dsw-alias-state-warning-primary);
242
+ color: var(--dsw-alias-state-warning-primary);
243
+ background: rgba(245, 158, 11, 0.08);
244
+ }
245
+ .dsh-goal-label {
246
+ font-weight: 600;
247
+ color: var(--dsw-alias-label-primary);
248
+ white-space: nowrap;
249
+ }
250
+ .dsh-goal-label.completed {
251
+ color: var(--dsw-alias-state-success-primary);
252
+ }
253
+ .dsh-goal-title {
254
+ color: var(--dsw-alias-label-secondary);
255
+ overflow: hidden;
256
+ text-overflow: ellipsis;
257
+ white-space: nowrap;
258
+ font-weight: 400;
259
+ flex: 1;
260
+ min-width: 0;
261
+ }
262
+ .dsh-goal-time {
263
+ color: var(--dsw-alias-label-secondary);
264
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
265
+ font-size: 12px;
266
+ font-weight: 500;
267
+ white-space: nowrap;
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: 4px;
271
+ flex-shrink: 0;
272
+ }
273
+ .dsh-goal-right {
274
+ display: flex;
275
+ align-items: center;
276
+ gap: 8px;
277
+ margin-left: 12px;
278
+ flex-shrink: 0;
279
+ }
280
+ .dsh-goal-btn {
281
+ appearance: none;
282
+ font: inherit;
283
+ background: var(--dsw-alias-bg-layer-2);
284
+ border: 1px solid var(--dsw-alias-border-l2);
285
+ padding: 5px 8px;
286
+ margin: 0;
287
+ color: var(--dsw-alias-label-primary);
288
+ cursor: pointer;
289
+ display: inline-flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ gap: 5px;
293
+ border-radius: 8px;
294
+ font-size: 12px;
295
+ font-weight: 500;
296
+ transition: all 0.15s ease;
297
+ }
298
+ .dsh-goal-btn:hover:not(:disabled) {
299
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
300
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
301
+ }
302
+ .dsh-goal-btn.icon-only {
303
+ padding: 4px 6px;
304
+ border-radius: 6px;
305
+ border-color: transparent;
306
+ background: transparent;
307
+ color: var(--dsw-alias-label-tertiary);
308
+ }
309
+ .dsh-goal-btn.icon-only:hover {
310
+ color: var(--dsw-alias-label-primary);
311
+ background: var(--dsw-alias-bg-layer-2);
312
+ border-color: var(--dsw-alias-border-l2);
313
+ }
314
+ .dsh-goal-btn.close {
315
+ font-size: 13px;
316
+ font-weight: bold;
317
+ padding: 3px 6px;
318
+ }
319
+ .dsh-goal-btn.close:hover {
320
+ color: var(--dsw-alias-state-error-primary);
321
+ }
322
+ .dsh-goal-btn-primary {
323
+ background: var(--dsw-alias-label-primary);
324
+ color: var(--dsw-alias-bg-layer-3);
325
+ border-color: transparent;
326
+ }
327
+ .dsh-goal-btn-primary:hover:not(:disabled) {
328
+ opacity: 0.88;
329
+ }
330
+ .dsh-goal-btn-danger {
331
+ color: var(--dsw-alias-state-error-primary);
332
+ border-color: rgba(239, 68, 68, 0.3);
333
+ }
334
+ .dsh-goal-btn-danger:hover:not(:disabled) {
335
+ background: rgba(239, 68, 68, 0.12) !important;
336
+ border-color: rgba(239, 68, 68, 0.5);
337
+ }
338
+
339
+ /* Modal Details */
340
+ .dsh-goal-modal-overlay {
341
+ position: fixed;
342
+ inset: 0;
343
+ background: rgba(0, 0, 0, 0.6);
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ z-index: 10000;
348
+ backdrop-filter: blur(4px);
349
+ }
350
+ .dsh-goal-modal {
351
+ background: var(--dsw-alias-bg-layer-3);
352
+ border: 1px solid var(--dsw-alias-border-l2);
353
+ border-radius: 12px;
354
+ width: 560px;
355
+ max-width: 90vw;
356
+ max-height: 80vh;
357
+ display: flex;
358
+ flex-direction: column;
359
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
360
+ }
361
+ .dsh-goal-modal-head {
362
+ padding: 16px 20px;
363
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: space-between;
367
+ }
368
+ .dsh-goal-modal-title {
369
+ font-size: 16px;
370
+ font-weight: 600;
371
+ color: var(--dsw-alias-label-primary);
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 8px;
375
+ }
376
+ .dsh-goal-modal-body {
377
+ padding: 18px 20px;
378
+ overflow-y: auto;
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 14px;
382
+ }
383
+ .dsh-goal-stat-grid {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
386
+ gap: 10px;
387
+ }
388
+ .dsh-goal-stat-box {
389
+ padding: 10px 12px;
390
+ border: 1px solid var(--dsw-alias-border-l2);
391
+ border-radius: 8px;
392
+ background: var(--dsw-alias-bg-layer-2);
393
+ display: flex;
394
+ flex-direction: column;
395
+ gap: 2px;
396
+ }
397
+ .dsh-goal-stat-val {
398
+ font-size: 16px;
399
+ font-weight: 700;
400
+ color: var(--dsw-alias-label-primary);
401
+ }
402
+ .dsh-goal-stat-lbl {
403
+ font-size: 11px;
404
+ color: var(--dsw-alias-label-secondary);
405
+ }
406
+ .dsh-goal-milestone-item {
407
+ display: flex;
408
+ align-items: flex-start;
409
+ gap: 10px;
410
+ padding: 10px 12px;
411
+ border-radius: 8px;
412
+ background: var(--dsw-alias-bg-layer-2);
413
+ border: 1px solid var(--dsw-alias-border-l2);
414
+ font-size: 13px;
415
+ transition: all 0.15s ease;
416
+ }
417
+ .dsh-goal-modal-foot {
418
+ padding: 14px 20px;
419
+ border-top: 1px solid var(--dsw-alias-border-l2);
420
+ display: flex;
421
+ justify-content: flex-end;
422
+ align-items: center;
423
+ gap: 10px;
424
+ }
425
+
426
+ /* Settings Card (clinebot pattern) */
427
+ .dsh-goal-card {
428
+ border: 1px solid var(--dsw-alias-border-l2);
429
+ background: var(--dsw-alias-bg-layer-3);
430
+ border-radius: 12px;
431
+ list-style: none;
432
+ margin-bottom: 12px;
433
+ transition: border-color 0.15s ease;
434
+ }
435
+ .dsh-goal-card:hover {
436
+ border-color: var(--dsw-alias-label-tertiary);
437
+ }
438
+ .dsh-goal-card-head {
439
+ appearance: none;
440
+ width: 100%;
441
+ font: inherit;
442
+ color: inherit;
443
+ text-align: left;
444
+ cursor: pointer;
445
+ background: transparent;
446
+ border: 0;
447
+ border-radius: 12px;
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 12px;
451
+ padding: 16px 20px;
452
+ }
453
+ .dsh-goal-card-title {
454
+ color: var(--dsw-alias-label-primary);
455
+ font-size: 15px;
456
+ font-weight: 600;
457
+ line-height: 1.4;
458
+ }
459
+ .dsh-goal-card-sub {
460
+ color: var(--dsw-alias-label-secondary);
461
+ font-size: 13px;
462
+ }
463
+ .dsh-goal-card-body {
464
+ border-top: 1px solid var(--dsw-alias-border-l2);
465
+ margin: 0 20px;
466
+ padding: 16px 0 16px;
467
+ display: flex;
468
+ flex-direction: column;
469
+ gap: 14px;
470
+ }
471
+ .dsh-goal-card-field {
472
+ display: flex;
473
+ flex-direction: column;
474
+ gap: 6px;
475
+ }
476
+ .dsh-goal-field-label-row {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: space-between;
480
+ font-size: 13px;
481
+ font-weight: 500;
482
+ color: var(--dsw-alias-label-primary);
483
+ }
484
+ .dsh-goal-field-meta {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 6px;
488
+ font-size: 11px;
489
+ color: var(--dsw-alias-label-tertiary);
490
+ }
491
+ .dsh-goal-override-tag {
492
+ font-size: 11px;
493
+ padding: 2px 6px;
494
+ border-radius: 4px;
495
+ background: var(--dsw-alias-bg-layer-2);
496
+ color: var(--dsw-alias-label-secondary);
497
+ border: 1px solid var(--dsw-alias-border-l2);
498
+ }
499
+ .dsh-goal-card-input {
500
+ height: 36px;
501
+ border: 1px solid var(--dsw-alias-border-l2);
502
+ background: var(--dsw-alias-bg-layer-2);
503
+ color: var(--dsw-alias-label-primary);
504
+ border-radius: 8px;
505
+ padding: 0 12px;
506
+ font-size: 13px;
507
+ width: 100%;
508
+ box-sizing: border-box;
509
+ }
510
+ .dsh-goal-card-input:focus {
511
+ outline: none;
512
+ border-color: var(--dsw-alias-state-brand-primary);
513
+ }
514
+ .dsh-goal-card-input[aria-invalid="true"] {
515
+ border-color: var(--dsw-alias-state-error-primary);
516
+ }
517
+ .dsh-goal-chev {
518
+ margin-left: auto;
519
+ flex: none;
520
+ color: var(--dsw-alias-label-tertiary);
521
+ transition: transform .16s ease;
522
+ }
523
+ .dsh-goal-chev-open {
524
+ transform: rotate(180deg);
525
+ }
526
+ .dsh-goal-check-row {
527
+ display: flex;
528
+ align-items: center;
529
+ justify-content: space-between;
530
+ padding: 6px 0;
531
+ }
532
+ .dsh-goal-check {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 8px;
536
+ font-size: 13px;
537
+ color: var(--dsw-alias-label-primary);
538
+ cursor: pointer;
539
+ }
540
+ .dsh-goal-foot {
541
+ border-top: 1px solid var(--dsw-alias-border-l2);
542
+ display: flex;
543
+ justify-content: flex-end;
544
+ align-items: center;
545
+ gap: 10px;
546
+ padding: 14px 0 4px;
547
+ }
548
+ .dsh-goal-foot-note {
549
+ color: var(--dsw-alias-state-error-primary);
550
+ font-size: 12px;
551
+ margin-right: auto;
552
+ }
553
+ .dsh-goal-btn-inline-reset {
554
+ background: transparent;
555
+ border: 0;
556
+ cursor: pointer;
557
+ padding: 2px 6px;
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 4px;
561
+ border-radius: 4px;
562
+ color: var(--dsw-alias-label-tertiary);
563
+ font-size: 11px;
564
+ }
565
+ .dsh-goal-btn-inline-reset:hover {
566
+ color: var(--dsw-alias-label-primary);
567
+ background: var(--dsw-alias-bg-layer-2);
568
+ }
569
+ .dsh-goal-discard {
570
+ appearance: none;
571
+ font: inherit;
572
+ cursor: pointer;
573
+ border: 1px solid var(--dsw-alias-border-l2);
574
+ border-radius: 8px;
575
+ padding: 6px 14px;
576
+ font-size: 13px;
577
+ background: var(--dsw-alias-bg-layer-2);
578
+ color: var(--dsw-alias-label-primary);
579
+ transition: all 0.15s ease;
580
+ }
581
+ .dsh-goal-discard:hover:not(:disabled) {
582
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
583
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
584
+ }
585
+ .dsh-goal-discard:disabled {
586
+ opacity: 0.4;
587
+ cursor: not-allowed;
588
+ }
589
+ .dsh-goal-save {
590
+ appearance: none;
591
+ font: inherit;
592
+ cursor: pointer;
593
+ border: 1px solid transparent;
594
+ border-radius: 8px;
595
+ padding: 6px 16px;
596
+ font-size: 13px;
597
+ background: var(--dsw-alias-label-primary);
598
+ color: var(--dsw-alias-bg-layer-3);
599
+ font-weight: 500;
600
+ transition: opacity 0.15s ease;
601
+ }
602
+ .dsh-goal-save:hover:not(:disabled) {
603
+ opacity: 0.88;
604
+ }
605
+ .dsh-goal-save:disabled {
606
+ opacity: 0.4;
607
+ cursor: not-allowed;
608
+ }
609
+ `;
610
+
611
+ function injectStylesOnce() {
612
+ if (typeof document === 'undefined') return;
613
+ if (document.getElementById('dsh-goal-styles')) return;
614
+ const style = document.createElement('style');
615
+ style.id = 'dsh-goal-styles';
616
+ style.dataset.dshPlugin = NS;
617
+ style.textContent = CSS_STYLES;
618
+ document.head.appendChild(style);
619
+ }
620
+
621
+
622
+ // Синтез приятных звуковых колокольчиков через Web Audio API (Item 6)
623
+ function playGoalChime(isSuccess = true) {
624
+ if (typeof window === 'undefined') return;
625
+ try {
626
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
627
+ if (!AudioCtx) return;
628
+ const actx = new AudioCtx();
629
+ if (actx.state === 'suspended') {
630
+ actx.resume().catch(() => {});
631
+ }
632
+ const now = actx.currentTime;
633
+ // Пентатонические аккорды: успех — восходящий C5-E5-G5-C6; сбой/ошибка — нисходящий G4-Eb4-C4
634
+ const notes = isSuccess
635
+ ? [523.25, 659.25, 783.99, 1046.50]
636
+ : [392.00, 311.13, 261.63];
637
+
638
+ notes.forEach((freq, idx) => {
639
+ const osc = actx.createOscillator();
640
+ const gain = actx.createGain();
641
+ osc.type = 'sine';
642
+ osc.frequency.setValueAtTime(freq, now + idx * 0.12);
643
+
644
+ gain.gain.setValueAtTime(0.001, now + idx * 0.12);
645
+ gain.gain.exponentialRampToValueAtTime(0.15, now + idx * 0.12 + 0.02);
646
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + idx * 0.12 + 0.4);
647
+
648
+ osc.connect(gain);
649
+ gain.connect(actx.destination);
650
+
651
+ osc.start(now + idx * 0.12);
652
+ osc.stop(now + idx * 0.12 + 0.45);
653
+ });
654
+
655
+ setTimeout(() => {
656
+ try { actx.close(); } catch (_) {}
657
+ }, 2000);
658
+ } catch (_) {}
659
+ }
660
+
661
+ // --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
662
+ function GoalDetailsModal({ state, onClose, onAction, t }) {
663
+ if (!state) return null;
664
+
665
+ const milestones = state.milestones || [];
666
+ const isCompleted = state.state === 'COMPLETED';
667
+
668
+ return React.createElement(
669
+ 'div',
670
+ { className: 'dsh-goal-modal-overlay', onClick: onClose },
671
+ React.createElement(
672
+ 'div',
673
+ {
674
+ className: 'dsh-goal-modal',
675
+ onClick: (e) => e.stopPropagation(),
676
+ },
677
+ React.createElement(
678
+ 'div',
679
+ { className: 'dsh-goal-modal-head' },
680
+ React.createElement(
681
+ 'div',
682
+ { className: 'dsh-goal-modal-title' },
683
+ isCompleted
684
+ ? React.createElement(IconCheck, { size: 18, className: 'cb-badge-ok' })
685
+ : React.createElement(IconTarget, { size: 18 }),
686
+ isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
687
+ ),
688
+ React.createElement(
689
+ 'button',
690
+ {
691
+ className: 'dsh-goal-btn icon-only close',
692
+ onClick: onClose,
693
+ title: t('close') || 'Закрыть',
694
+ },
695
+ '✕',
696
+ ),
697
+ ),
698
+ React.createElement(
699
+ 'div',
700
+ { className: 'dsh-goal-modal-body' },
701
+ React.createElement(
702
+ 'div',
703
+ null,
704
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 16, marginBottom: 4, color: 'var(--dsw-alias-label-primary)' } }, state.title),
705
+ state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 6, lineHeight: 1.4 } }, state.description) : null,
706
+ ),
707
+ React.createElement(
708
+ 'div',
709
+ { className: 'dsh-goal-stat-grid' },
710
+ React.createElement(
711
+ 'div',
712
+ { className: 'dsh-goal-stat-box' },
713
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedElapsed || '0s'),
714
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('time') || 'Время работы'),
715
+ ),
716
+ React.createElement(
717
+ 'div',
718
+ { className: 'dsh-goal-stat-box' },
719
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.iterationsCount || 0}/${state.maxIterations || 25}`),
720
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Итерации'),
721
+ ),
722
+ React.createElement(
723
+ 'div',
724
+ { className: 'dsh-goal-stat-box' },
725
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.progressPercent || 0}%`),
726
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Прогресс'),
727
+ ),
728
+ ),
729
+ React.createElement(
730
+ 'div',
731
+ { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
732
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('milestones') || 'План работ:'),
733
+ milestones.length === 0
734
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
735
+ : milestones.map((m, i) =>
736
+ React.createElement(
737
+ 'div',
738
+ { key: m.id || i, className: 'dsh-goal-milestone-item' },
739
+ React.createElement('span', { style: { fontSize: 15 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
740
+ React.createElement(
741
+ 'div',
742
+ { style: { flex: 1, minWidth: 0 } },
743
+ React.createElement(
744
+ 'div',
745
+ {
746
+ style: {
747
+ textDecoration: m.status === 'completed' ? 'line-through' : 'none',
748
+ color: m.status === 'completed' ? 'var(--dsw-alias-label-tertiary)' : 'inherit',
749
+ fontWeight: m.status === 'in_progress' ? 600 : 400,
750
+ },
751
+ },
752
+ `${i + 1}. ${m.title}`,
753
+ ),
754
+ m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
755
+ ),
756
+ ),
757
+ ),
758
+ ),
759
+ ),
760
+ React.createElement(
761
+ 'div',
762
+ { className: 'dsh-goal-modal-foot' },
763
+ isCompleted
764
+ ? React.createElement(
765
+ 'button',
766
+ {
767
+ className: 'dsh-goal-btn dsh-goal-btn-danger',
768
+ onClick: () => {
769
+ onAction('clear');
770
+ onClose();
771
+ },
772
+ },
773
+ t('closeBanner') || 'Закрыть плашку цели',
774
+ )
775
+ : null,
776
+ React.createElement(
777
+ 'button',
778
+ {
779
+ className: 'dsh-goal-btn dsh-goal-btn-primary',
780
+ onClick: onClose,
781
+ },
782
+ t('close') || 'Закрыть',
783
+ ),
784
+ ),
785
+ ),
786
+ );
787
+ }
788
+
789
+ function sessionIdOf(ctx, props) {
790
+ try {
791
+ if (props?.session?.id) return String(props.session.id);
792
+ if (props?.session?.sessionId) return String(props.session.sessionId);
793
+ if (props?.input?.sessionId) return String(props.input.sessionId);
794
+ if (ctx?.scope?.session) return String(ctx.scope.session.id || ctx.scope.session.header?.id || '');
795
+ if (typeof ctx?.scope?.id === 'string') return ctx.scope.id;
796
+ if (ctx?.session) return String(ctx.session.id || ctx.session.header?.id || '');
797
+ if (typeof window !== 'undefined' && window.location) {
798
+ const params = new URLSearchParams(window.location.search);
799
+ const fromUrl = params.get('session') || params.get('sessionId');
800
+ if (fromUrl) return fromUrl;
801
+ }
802
+ } catch (_) {}
803
+ return 'default';
804
+ }
805
+
806
+ // --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
807
+ function GoalTopBanner(props) {
808
+ const { ctx } = props || {};
809
+ const sid = sessionIdOf(ctx, props);
810
+ const [state, setState] = useState(null);
811
+ const [isModalOpen, setIsModalOpen] = useState(false);
812
+ const [now, setNow] = useState(() => Date.now());
813
+ const stateRef = useRef(null);
814
+ stateRef.current = state;
815
+ const lastStateRef = useRef(null);
816
+
817
+ const t = (key) => {
818
+ if (ctx?.locale?.getSnapshot) {
819
+ const snap = ctx.locale.getSnapshot();
820
+ const active = snap?.active || 'ru';
821
+ return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
822
+ }
823
+ return LOCALES.ru[key] || LOCALES.en[key] || key;
824
+ };
825
+
826
+ useEffect(() => {
827
+ injectStylesOnce();
828
+ }, []);
829
+
830
+ // Локальный секундный интервал для плавного и бесперебойного тикания таймера
831
+ useEffect(() => {
832
+ const ticker = setInterval(() => {
833
+ setNow(Date.now());
834
+ }, 1000);
835
+ return () => clearInterval(ticker);
836
+ }, []);
837
+
838
+ const fetchState = useCallback(async () => {
839
+ try {
840
+ const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
841
+ const res = await fetch(`/dsh-goal/state${query}`, {
842
+ headers: { 'x-dsh-session-id': sid },
843
+ });
844
+ if (res.ok) {
845
+ const data = await res.json();
846
+ updateStateWithAudio(data);
847
+ return data;
848
+ }
849
+ } catch (_) {}
850
+ }, [sid]);
851
+
852
+ // Звуковое оповещение при переходе в COMPLETED или FAILED
853
+ const updateStateWithAudio = useCallback((nextState) => {
854
+ if (nextState && lastStateRef.current) {
855
+ const prev = lastStateRef.current;
856
+ if (prev.state !== nextState.state && nextState.enableSound !== false) {
857
+ if (nextState.state === 'COMPLETED') {
858
+ playGoalChime(true);
859
+ } else if (nextState.state === 'FAILED') {
860
+ playGoalChime(false);
861
+ }
862
+ }
863
+ }
864
+ lastStateRef.current = nextState;
865
+ setState(nextState);
866
+ }, []);
867
+
868
+ // Realtime Server-Sent Events (SSE) с автоматическим адаптивным fallback-поллингом
869
+ useEffect(() => {
870
+ let es = null;
871
+ let timer = null;
872
+ let isDisposed = false;
873
+ let sseActive = false;
874
+
875
+ const scheduleNextPoll = () => {
876
+ if (isDisposed || sseActive) return;
877
+ const currentState = stateRef.current;
878
+ const isDocHidden = typeof document !== 'undefined' && document.hidden;
879
+ const isRunning = currentState?.hasActiveGoal && currentState?.state === 'RUNNING';
880
+
881
+ let delay = 1500;
882
+ if (isDocHidden) {
883
+ delay = 10000;
884
+ } else if (!isRunning) {
885
+ delay = 8000;
886
+ }
887
+
888
+ timer = setTimeout(async () => {
889
+ await fetchState();
890
+ scheduleNextPoll();
891
+ }, delay);
892
+ };
893
+
894
+ // Запуск SSE соединения
895
+ if (typeof window !== 'undefined' && typeof window.EventSource !== 'undefined') {
896
+ try {
897
+ const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
898
+ es = new window.EventSource(`/dsh-goal/events${query}`);
899
+
900
+ es.onopen = () => {
901
+ sseActive = true;
902
+ if (timer) {
903
+ clearTimeout(timer);
904
+ timer = null;
905
+ }
906
+ };
907
+
908
+ es.onmessage = (event) => {
909
+ if (isDisposed) return;
910
+ try {
911
+ const data = JSON.parse(event.data);
912
+ if (data && typeof data === 'object') {
913
+ updateStateWithAudio(data);
914
+ }
915
+ } catch (_) {}
916
+ };
917
+
918
+ es.onerror = () => {
919
+ // При сбое соединения SSE переключаемся на адаптивный fallback поллинг
920
+ sseActive = false;
921
+ if (!isDisposed && !timer) {
922
+ scheduleNextPoll();
923
+ }
924
+ };
925
+ } catch (_) {
926
+ sseActive = false;
927
+ scheduleNextPoll();
928
+ }
929
+ } else {
930
+ scheduleNextPoll();
931
+ }
932
+
933
+ // Первоначальная загрузка состояния
934
+ fetchState().then((data) => {
935
+ if (!sseActive) scheduleNextPoll();
936
+ });
937
+
938
+ const onVisibilityChange = () => {
939
+ if (typeof document !== 'undefined' && !document.hidden) {
940
+ if (!sseActive) {
941
+ if (timer) clearTimeout(timer);
942
+ fetchState().then(() => {
943
+ scheduleNextPoll();
944
+ });
945
+ }
946
+ }
947
+ };
948
+
949
+ if (typeof document !== 'undefined' && document.addEventListener) {
950
+ document.addEventListener('visibilitychange', onVisibilityChange);
951
+ }
952
+
953
+ return () => {
954
+ isDisposed = true;
955
+ if (es) {
956
+ es.close();
957
+ es = null;
958
+ }
959
+ if (timer) clearTimeout(timer);
960
+ if (typeof document !== 'undefined' && document.removeEventListener) {
961
+ document.removeEventListener('visibilitychange', onVisibilityChange);
962
+ }
963
+ };
964
+ }, [sid, fetchState, updateStateWithAudio]);
965
+
966
+ const handleAction = async (action, extra = {}) => {
967
+ try {
968
+ const res = await fetch('/dsh-goal/action', {
969
+ method: 'POST',
970
+ headers: {
971
+ 'Content-Type': 'application/json',
972
+ 'x-dsh-session-id': sid,
973
+ },
974
+ body: JSON.stringify({ action, sessionId: sid, ...extra }),
975
+ });
976
+ if (res.ok) {
977
+ const data = await res.json();
978
+ if (data.state) updateStateWithAudio(data.state);
979
+ }
980
+ } catch (_) {}
981
+ };
982
+
983
+ if (!state || !state.hasActiveGoal) {
984
+ return null;
985
+ }
986
+
987
+ const isPaused = state.state === 'PAUSED';
988
+ const isCompleted = state.state === 'COMPLETED';
989
+
990
+ const formatLiveElapsed = () => {
991
+ if (!state) return '0s';
992
+ if (!state.startedAt) return state.formattedElapsed || '0s';
993
+ const endTime = state.completedAt || (state.pausedAt || now);
994
+ const elapsedSec = Math.max(0, Math.floor((endTime - state.startedAt - (state.totalPausedDurationMs || 0)) / 1000));
995
+ return formatElapsed(elapsedSec);
996
+ };
997
+
998
+ const liveElapsedStr = formatLiveElapsed();
999
+
1000
+ return React.createElement(
1001
+ React.Fragment,
1002
+ null,
1003
+ React.createElement(
1004
+ 'div',
1005
+ { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
1006
+ React.createElement(
1007
+ 'div',
1008
+ { className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
1009
+ React.createElement(
1010
+ 'div',
1011
+ { className: 'dsh-goal-left' },
1012
+ React.createElement(
1013
+ 'span',
1014
+ { className: 'dsh-goal-icon' },
1015
+ isCompleted ? React.createElement(IconCheck, { size: 16 }) : React.createElement(IconTarget, { size: 15 }),
1016
+ ),
1017
+ React.createElement(
1018
+ 'span',
1019
+ {
1020
+ className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isPaused ? 'dsh-goal-badge-warn' : ''}`,
1021
+ },
1022
+ isCompleted ? (t('goalCompleted') || 'Цель выполнена') : isPaused ? (t('pause') || 'На паузе') : (t('goalLabel') || 'Текущая цель'),
1023
+ ),
1024
+ React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
1025
+ ),
1026
+ React.createElement(
1027
+ 'div',
1028
+ { className: 'dsh-goal-right' },
1029
+ React.createElement(
1030
+ 'span',
1031
+ { className: 'dsh-goal-time' },
1032
+ `⏱ ${liveElapsedStr}`,
1033
+ ),
1034
+ !isCompleted
1035
+ ? React.createElement(
1036
+ 'button',
1037
+ {
1038
+ className: 'dsh-goal-btn icon-only',
1039
+ title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
1040
+ onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
1041
+ },
1042
+ isPaused ? React.createElement(IconPlay, { size: 14 }) : React.createElement(IconPause, { size: 14 }),
1043
+ )
1044
+ : null,
1045
+ React.createElement(
1046
+ 'button',
1047
+ {
1048
+ className: 'dsh-goal-btn icon-only',
1049
+ title: t('details') || 'Развернуть детали цели',
1050
+ onClick: () => setIsModalOpen(true),
1051
+ },
1052
+ React.createElement(IconExpand, { size: 14 }),
1053
+ ),
1054
+ isCompleted
1055
+ ? React.createElement(
1056
+ 'button',
1057
+ {
1058
+ className: 'dsh-goal-btn icon-only close',
1059
+ title: t('closeBanner') || 'Закрыть плашку цели',
1060
+ onClick: () => handleAction('clear'),
1061
+ },
1062
+ '✕',
1063
+ )
1064
+ : null,
1065
+ ),
1066
+ ),
1067
+ ),
1068
+ isModalOpen
1069
+ ? React.createElement(GoalDetailsModal, {
1070
+ state,
1071
+ onClose: () => setIsModalOpen(false),
1072
+ onAction: handleAction,
1073
+ t,
1074
+ })
1075
+ : null,
1076
+ );
1077
+ }
1078
+
1079
+ // --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
1080
+ const DEFAULT_SETTINGS = {
1081
+ maxIterations: 25,
1082
+ autoDrive: true,
1083
+ enableSound: true,
1084
+ };
1085
+
1086
+ function parseNumberField(raw) {
1087
+ if (raw === '' || raw === null || raw === undefined) {
1088
+ return undefined;
1089
+ }
1090
+ const n = Number(raw);
1091
+ if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
1092
+ return NaN;
1093
+ }
1094
+ return n;
1095
+ }
1096
+
1097
+ function getFieldStatus(field, draftValue, snap) {
1098
+ const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
1099
+ const userVal = snap?.user?.[field];
1100
+ const effectiveVal = snap?.value?.[field] ?? baseVal;
1101
+
1102
+ const isOverridden = userVal !== undefined && userVal !== null;
1103
+ const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
1104
+
1105
+ let invalid = false;
1106
+ let isDirty = false;
1107
+
1108
+ if (field === 'maxIterations') {
1109
+ if (draftValue !== undefined) {
1110
+ if (draftValue === '') {
1111
+ invalid = false;
1112
+ isDirty = userVal !== undefined;
1113
+ } else {
1114
+ const parsed = parseNumberField(draftValue);
1115
+ if (Number.isNaN(parsed)) {
1116
+ invalid = true;
1117
+ isDirty = true;
1118
+ } else {
1119
+ isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
1120
+ }
1121
+ }
1122
+ }
1123
+ } else {
1124
+ if (draftValue !== undefined) {
1125
+ isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
1126
+ }
1127
+ }
1128
+
1129
+ return {
1130
+ value: currentVal,
1131
+ baseValue: baseVal,
1132
+ userValue: userVal,
1133
+ isOverridden,
1134
+ isDirty,
1135
+ invalid,
1136
+ };
1137
+ }
1138
+
1139
+ function computeSavePlan(draft, snap) {
1140
+ if (!draft) return [];
1141
+
1142
+ const fields = ['maxIterations', 'autoDrive', 'enableSound'];
1143
+ const writes = [];
1144
+
1145
+ for (const f of fields) {
1146
+ if (draft[f] === undefined) continue;
1147
+
1148
+ const status = getFieldStatus(f, draft[f], snap);
1149
+ if (status.invalid) continue;
1150
+
1151
+ if (status.isDirty) {
1152
+ if (f === 'maxIterations' && draft[f] === '') {
1153
+ writes.push([f, undefined]);
1154
+ } else if (f === 'maxIterations') {
1155
+ writes.push([f, parseNumberField(draft[f])]);
1156
+ } else {
1157
+ writes.push([f, Boolean(draft[f])]);
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ return writes;
1163
+ }
1164
+
1165
+ // --- КАРТОЧКА НАСТРОЕК (SETTINGS CARD) ---
1166
+ function GoalSettingsCard(props) {
1167
+ const { scope, ctx } = props || {};
1168
+ const [isOpen, setIsOpen] = useState(false);
1169
+ const [draft, setDraft] = useState(null);
1170
+ const [saving, setSaving] = useState(false);
1171
+ const [failed, setFailed] = useState(false);
1172
+
1173
+ const [snap, setSnap] = useState(() => (scope ? scope.getSnapshot() : { value: DEFAULT_SETTINGS, base: DEFAULT_SETTINGS, user: {} }));
1174
+
1175
+ useEffect(() => {
1176
+ injectStylesOnce();
1177
+ }, []);
1178
+
1179
+ useEffect(() => {
1180
+ if (!scope || typeof scope.subscribe !== 'function') return;
1181
+ return scope.subscribe((next) => {
1182
+ setSnap(next || scope.getSnapshot());
1183
+ });
1184
+ }, [scope]);
1185
+
1186
+ const t = (key) => {
1187
+ if (ctx?.locale?.getSnapshot) {
1188
+ const snapLoc = ctx.locale.getSnapshot();
1189
+ const active = snapLoc?.active || 'ru';
1190
+ return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
1191
+ }
1192
+ return LOCALES.ru[key] || LOCALES.en[key] || key;
1193
+ };
1194
+
1195
+ const status = (snap && snap.status) || 'ready';
1196
+ const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
1197
+ const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
1198
+ const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
1199
+
1200
+ const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
1201
+ const invalid = maxIterStatus.invalid;
1202
+ const disabled = !scope || saving || snap?.writable === false;
1203
+
1204
+ const edit = (field, val) => {
1205
+ setFailed(false);
1206
+ setDraft((prev) => ({
1207
+ maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
1208
+ autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
1209
+ enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
1210
+ [field]: val,
1211
+ }));
1212
+ };
1213
+
1214
+ const resetFieldToDefault = (field) => {
1215
+ if (field === 'maxIterations') {
1216
+ edit(field, '');
1217
+ } else {
1218
+ edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
1219
+ }
1220
+ };
1221
+
1222
+ const save = async () => {
1223
+ if (!scope || !dirty || invalid) return;
1224
+ setSaving(true);
1225
+ setFailed(false);
1226
+
1227
+ const writes = computeSavePlan(draft, snap);
1228
+ let landed = true;
1229
+
1230
+ for (const [field, fieldValue] of writes) {
1231
+ try {
1232
+ await scope.set(field, fieldValue);
1233
+ } catch (err) {
1234
+ console.error('[dsh-goal] failed to save setting', field, err);
1235
+ landed = false;
1236
+ }
1237
+ }
1238
+
1239
+ const fresh = scope.getSnapshot();
1240
+ const user = fresh.user || {};
1241
+ for (const [field, fieldValue] of writes) {
1242
+ if (fieldValue === undefined) {
1243
+ if (user[field] !== undefined) landed = false;
1244
+ } else {
1245
+ if (user[field] !== fieldValue) landed = false;
1246
+ }
1247
+ }
1248
+
1249
+ setSnap(fresh);
1250
+ setSaving(false);
1251
+ if (landed) {
1252
+ setDraft(null);
1253
+ } else {
1254
+ setFailed(true);
1255
+ }
1256
+ };
1257
+
1258
+ const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
1259
+
1260
+ return React.createElement(
1261
+ 'li',
1262
+ { className: 'dsh-goal-card' },
1263
+ React.createElement(
1264
+ 'button',
1265
+ {
1266
+ className: 'dsh-goal-card-head',
1267
+ onClick: () => setIsOpen(!isOpen),
1268
+ 'aria-expanded': isOpen,
1269
+ },
1270
+ React.createElement(
1271
+ 'div',
1272
+ { style: { flex: 1 } },
1273
+ React.createElement('div', { className: 'dsh-goal-card-title' }, t('pluginTitle') || 'Цели и автономный режим (Goal Mode)'),
1274
+ React.createElement('div', { className: 'dsh-goal-card-sub' }, t('pluginDesc') || 'Верхняя закреплённая панель цели, декомпозиция вех и авто-драйв'),
1275
+ ),
1276
+ React.createElement(Chevron, { className: `dsh-goal-chev ${isOpen ? 'dsh-goal-chev-open' : ''}` }),
1277
+ ),
1278
+ isOpen
1279
+ ? React.createElement(
1280
+ 'div',
1281
+ { className: 'dsh-goal-card-body' },
1282
+ status === 'loading'
1283
+ ? React.createElement('p', { className: 'dsh-goal-field-hint', style: { padding: '12px 0' } }, t('loading') || 'Загрузка настроек…')
1284
+ : status !== 'ready'
1285
+ ? React.createElement('p', { className: 'dsh-goal-foot-note', style: { padding: '12px 0' } }, t('unavailable') || 'Настройки недоступны')
1286
+ : React.createElement(
1287
+ React.Fragment,
1288
+ null,
1289
+ React.createElement(
1290
+ 'div',
1291
+ { className: 'dsh-goal-card-field' },
1292
+ React.createElement(
1293
+ 'div',
1294
+ { className: 'dsh-goal-field-label-row' },
1295
+ React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1296
+ React.createElement(
1297
+ 'div',
1298
+ { className: 'dsh-goal-field-meta' },
1299
+ maxIterStatus.isOverridden
1300
+ ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1301
+ : null,
1302
+ maxIterStatus.isOverridden
1303
+ ? React.createElement(
1304
+ 'button',
1305
+ {
1306
+ type: 'button',
1307
+ className: 'dsh-goal-btn-inline-reset',
1308
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1309
+ onClick: () => resetFieldToDefault('maxIterations'),
1310
+ },
1311
+ React.createElement(IconRotateCcw, { size: 12 }),
1312
+ t('resetField') || 'По умолчанию',
1313
+ )
1314
+ : null,
1315
+ ),
1316
+ ),
1317
+ React.createElement('input', {
1318
+ id: 'dsh-goal-max-iter',
1319
+ type: 'number',
1320
+ min: 1,
1321
+ placeholder: String(maxIterStatus.baseValue),
1322
+ className: 'dsh-goal-card-input',
1323
+ value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1324
+ disabled,
1325
+ 'aria-invalid': invalid,
1326
+ onChange: (e) => edit('maxIterations', e.target.value),
1327
+ }),
1328
+ ),
1329
+ React.createElement(
1330
+ 'div',
1331
+ { className: 'dsh-goal-check-row' },
1332
+ React.createElement(
1333
+ 'label',
1334
+ { className: 'dsh-goal-check' },
1335
+ React.createElement('input', {
1336
+ type: 'checkbox',
1337
+ checked: autoDriveStatus.value === true,
1338
+ disabled,
1339
+ onChange: (e) => edit('autoDrive', e.target.checked),
1340
+ }),
1341
+ t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1342
+ ),
1343
+ autoDriveStatus.isOverridden
1344
+ ? React.createElement(
1345
+ 'button',
1346
+ {
1347
+ type: 'button',
1348
+ className: 'dsh-goal-btn-inline-reset',
1349
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1350
+ onClick: () => resetFieldToDefault('autoDrive'),
1351
+ },
1352
+ React.createElement(IconRotateCcw, { size: 12 }),
1353
+ )
1354
+ : null,
1355
+ ),
1356
+ React.createElement(
1357
+ 'div',
1358
+ { className: 'dsh-goal-check-row' },
1359
+ React.createElement(
1360
+ 'label',
1361
+ { className: 'dsh-goal-check' },
1362
+ React.createElement('input', {
1363
+ type: 'checkbox',
1364
+ checked: enableSoundStatus.value === true,
1365
+ disabled,
1366
+ onChange: (e) => edit('enableSound', e.target.checked),
1367
+ }),
1368
+ t('soundLabel') || 'Звук по завершении цели',
1369
+ ),
1370
+ enableSoundStatus.isOverridden
1371
+ ? React.createElement(
1372
+ 'button',
1373
+ {
1374
+ type: 'button',
1375
+ className: 'dsh-goal-btn-inline-reset',
1376
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1377
+ onClick: () => resetFieldToDefault('enableSound'),
1378
+ },
1379
+ React.createElement(IconRotateCcw, { size: 12 }),
1380
+ )
1381
+ : null,
1382
+ ),
1383
+ React.createElement(
1384
+ 'div',
1385
+ { className: 'dsh-goal-foot' },
1386
+ failed
1387
+ ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1388
+ : null,
1389
+ React.createElement(
1390
+ 'button',
1391
+ {
1392
+ className: 'dsh-goal-discard',
1393
+ disabled: !dirty || disabled,
1394
+ onClick: () => {
1395
+ setFailed(false);
1396
+ setDraft(null);
1397
+ },
1398
+ },
1399
+ t('discard') || 'Отменить правки',
1400
+ ),
1401
+ React.createElement(
1402
+ 'button',
1403
+ { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1404
+ saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1405
+ ),
1406
+ ),
1407
+ ),
1408
+ )
1409
+ : null,
1410
+ );
1411
+ }
1412
+
1413
+ // --- СЛОВАРИ ЛОКАЛИЗАЦИИ ---
1414
+ const LOCALES = {
1415
+ ru: {
1416
+ goalLabel: 'Текущая цель',
1417
+ goalCompleted: 'Цель выполнена',
1418
+ clearGoal: 'Очистить цель',
1419
+ closeBanner: 'Закрыть плашку цели',
1420
+ pause: 'Приостановить',
1421
+ resume: 'Возобновить',
1422
+ details: 'Развернуть детали цели',
1423
+ modalTitle: 'План и статус цели',
1424
+ modalTitleCompleted: 'Цель выполнена: план и результаты',
1425
+ time: 'Время работы',
1426
+ milestones: 'План работ:',
1427
+ noMilestones: 'Агент формирует план работ...',
1428
+ close: 'Закрыть',
1429
+ pluginTitle: 'Цели и автономный режим (Goal Mode)',
1430
+ pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
1431
+ maxIterLabel: 'Максимум итераций (Safety Limit):',
1432
+ autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
1433
+ soundLabel: 'Звук по завершении цели',
1434
+ save: 'Сохранить',
1435
+ saving: 'Сохранение…',
1436
+ discard: 'Отменить правки',
1437
+ resetField: 'По умолчанию',
1438
+ overridden: 'изменено',
1439
+ saveFailed: 'Не сохранено — исправьте и повторите',
1440
+ loading: 'Загрузка настроек…',
1441
+ unavailable: 'Настройки недоступны',
1442
+ },
1443
+ en: {
1444
+ goalLabel: 'Current goal',
1445
+ goalCompleted: 'Goal completed',
1446
+ clearGoal: 'Clear goal',
1447
+ closeBanner: 'Close goal banner',
1448
+ pause: 'Pause',
1449
+ resume: 'Resume',
1450
+ details: 'Expand Goal Details',
1451
+ modalTitle: 'Goal Plan & Status',
1452
+ modalTitleCompleted: 'Goal Completed: Plan & Results',
1453
+ time: 'Running time',
1454
+ milestones: 'Plan of work:',
1455
+ noMilestones: 'Agent is preparing the plan of work...',
1456
+ close: 'Close',
1457
+ pluginTitle: 'Goal Mode & Autonomous Loop',
1458
+ pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
1459
+ maxIterLabel: 'Max iterations (Safety Limit):',
1460
+ autoDriveLabel: 'Auto-drive: keep the loop running automatically',
1461
+ soundLabel: 'Sound when a goal completes',
1462
+ save: 'Save',
1463
+ saving: 'Saving…',
1464
+ discard: 'Discard changes',
1465
+ resetField: 'Default',
1466
+ overridden: 'overridden',
1467
+ saveFailed: 'Not saved — fix the values and try again',
1468
+ loading: 'Loading settings…',
1469
+ unavailable: 'Settings unavailable',
1470
+ },
1471
+ };
1472
+
1473
+ module.exports.inject = ['slots', 'locale', 'settingsScope'];
1474
+ module.exports.apply = function apply(ctx) {
1475
+ // Регистрация локализации
1476
+ try {
1477
+ ctx.locale?.register?.(NS, LOCALES);
1478
+ } catch (_) {}
1479
+
1480
+ // Пространство настроек плагина
1481
+ const settingsScope =
1482
+ ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
1483
+ ? ctx.settingsScope.bind({ namespace: NS })
1484
+ : null;
1485
+
1486
+ const registerSlotSafe = (name, entry, comp) => {
1487
+ try {
1488
+ if (typeof ctx.slots?.inject === 'function') {
1489
+ ctx.slots.inject(name, () => {
1490
+ try {
1491
+ return ctx.slots.register(entry, comp);
1492
+ } catch (_) {}
1493
+ });
1494
+ } else if (typeof ctx.slots?.register === 'function') {
1495
+ ctx.slots.register(entry, comp);
1496
+ }
1497
+ } catch (_) {}
1498
+ };
1499
+
1500
+ // 1. Регистрация виджета цели над полем ввода (composer dock)
1501
+ registerSlotSafe(
1502
+ 'conversation.input.dock',
1503
+ {
1504
+ name: 'conversation.input.dock',
1505
+ id: '@goodandready/dsh-goal',
1506
+ order: 10,
1507
+ locale: NS,
1508
+ inject: (slotProps) => ({ ctx, ...slotProps }),
1509
+ },
1510
+ GoalTopBanner,
1511
+ );
1512
+
1513
+ // 2. Регистрация карточки настроек
1514
+ registerSlotSafe(
1515
+ 'settings.plugin.item',
1516
+ {
1517
+ name: 'settings.plugin.item',
1518
+ key: NS,
1519
+ locale: NS,
1520
+ inject: () => ({ ctx, scope: settingsScope }),
1521
+ },
1522
+ GoalSettingsCard,
1523
+ );
1524
+ };
1525
+
1526
+ return module.exports;
1527
+ },
1528
+ });