@goodandready/dsh-goal 0.1.2 → 0.1.4

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,1235 +1,1305 @@
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 } = 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
- style: { color: 'var(--dsw-alias-status-success)' },
112
- className,
113
- },
114
- React.createElement('polyline', { points: '20 6 9 17 4 12' }),
115
- );
116
- }
117
-
118
- function IconExpand({ size = 14, className = '' }) {
119
- return React.createElement(
120
- 'svg',
121
- {
122
- width: size,
123
- height: size,
124
- viewBox: '0 0 24 24',
125
- fill: 'none',
126
- stroke: 'currentColor',
127
- strokeWidth: 1.8,
128
- strokeLinecap: 'round',
129
- strokeLinejoin: 'round',
130
- className,
131
- },
132
- React.createElement('polyline', { points: '15 3 21 3 21 9' }),
133
- React.createElement('polyline', { points: '9 21 3 21 3 15' }),
134
- React.createElement('line', { x1: 21, y1: 3, x2: 14, y2: 10 }),
135
- React.createElement('line', { x1: 3, y1: 21, x2: 10, y2: 14 }),
136
- );
137
- }
138
-
139
- function IconRotateCcw({ size = 13, className = '' }) {
140
- return React.createElement(
141
- 'svg',
142
- {
143
- width: size,
144
- height: size,
145
- viewBox: '0 0 24 24',
146
- fill: 'none',
147
- stroke: 'currentColor',
148
- strokeWidth: 2,
149
- strokeLinecap: 'round',
150
- strokeLinejoin: 'round',
151
- className,
152
- },
153
- React.createElement('polyline', { points: '1 4 1 10 7 10' }),
154
- React.createElement('path', { d: 'M3.51 15a9 9 0 1 0 2.13-9.36L1 10' }),
155
- );
156
- }
157
-
158
- function IconChevronDown({ size = 14, className = '' }) {
159
- return React.createElement(
160
- 'svg',
161
- {
162
- width: size,
163
- height: size,
164
- viewBox: '0 0 24 24',
165
- fill: 'none',
166
- stroke: 'currentColor',
167
- strokeWidth: 2,
168
- strokeLinecap: 'round',
169
- strokeLinejoin: 'round',
170
- className,
171
- },
172
- React.createElement('polyline', { points: '6 9 12 15 18 9' }),
173
- );
174
- }
175
-
176
- // --- СТИЛИ И CSS ПРАВИЛА (ЧИСТЫЕ DSH ТОКЕНЫ БЕЗ ХАРДКОД-ФОЛЛБЕКОВ) ---
177
- const CSS_STYLES = `
178
- .dsh-goal-dock {
179
- box-sizing: border-box;
180
- width: calc(100% - var(--dsh-composer-side-clearance, 0px) * 2 - var(--dsh-composer-dock-inset, 0px) * 4);
181
- max-width: calc(var(--dsh-composer-card-max-width, 768px) - var(--dsh-composer-dock-inset, 0px) * 4);
182
- margin: 0 auto 8px auto;
183
- display: flex;
184
- justify-content: center;
185
- }
186
- .dsh-goal-banner {
187
- box-sizing: border-box;
188
- width: 100%;
189
- display: flex;
190
- align-items: center;
191
- justify-content: space-between;
192
- background: var(--dsw-alias-bg-layer-2);
193
- border: 1px solid var(--dsw-alias-border-l2);
194
- border-radius: 12px;
195
- padding: 6px 14px;
196
- font-family: inherit;
197
- font-size: 13px;
198
- color: var(--dsw-alias-label-primary);
199
- user-select: none;
200
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
201
- transition: all 0.2s ease;
202
- }
203
- .dsh-goal-left {
204
- display: flex;
205
- align-items: center;
206
- gap: 8px;
207
- min-width: 0;
208
- flex: 1;
209
- }
210
- .dsh-goal-icon {
211
- color: var(--dsw-alias-label-secondary);
212
- flex-shrink: 0;
213
- display: flex;
214
- align-items: center;
215
- }
216
- .dsh-goal-label {
217
- font-weight: 600;
218
- color: var(--dsw-alias-label-primary);
219
- white-space: nowrap;
220
- }
221
- .dsh-goal-label.completed {
222
- color: var(--dsw-alias-status-success);
223
- }
224
- .dsh-goal-title {
225
- color: var(--dsw-alias-label-secondary);
226
- overflow: hidden;
227
- text-overflow: ellipsis;
228
- white-space: nowrap;
229
- font-weight: 400;
230
- flex: 1;
231
- min-width: 0;
232
- }
233
- .dsh-goal-time {
234
- color: var(--dsw-alias-label-secondary);
235
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
236
- font-size: 12px;
237
- font-weight: 500;
238
- white-space: nowrap;
239
- display: inline-flex;
240
- align-items: center;
241
- gap: 4px;
242
- flex-shrink: 0;
243
- }
244
- .dsh-goal-right {
245
- display: flex;
246
- align-items: center;
247
- gap: 8px;
248
- margin-left: 12px;
249
- flex-shrink: 0;
250
- }
251
- .dsh-goal-btn {
252
- background: transparent;
253
- border: 0;
254
- padding: 4px;
255
- margin: 0;
256
- color: var(--dsw-alias-label-tertiary);
257
- cursor: pointer;
258
- display: flex;
259
- align-items: center;
260
- justify-content: center;
261
- border-radius: 6px;
262
- transition: all 0.15s ease;
263
- }
264
- .dsh-goal-btn:hover {
265
- color: var(--dsw-alias-label-primary);
266
- background: var(--dsw-alias-bg-layer-3);
267
- }
268
- .dsh-goal-btn.close {
269
- font-size: 13px;
270
- font-weight: bold;
271
- padding: 3px 6px;
272
- }
273
- .dsh-goal-btn.close:hover {
274
- color: var(--dsw-alias-status-danger);
275
- }
276
- .dsh-goal-btn.danger:hover {
277
- color: var(--dsw-alias-status-danger);
278
- }
279
-
280
- /* Modal Details */
281
- .dsh-goal-modal-overlay {
282
- position: fixed;
283
- inset: 0;
284
- background: rgba(0, 0, 0, 0.6);
285
- display: flex;
286
- align-items: center;
287
- justify-content: center;
288
- z-index: 10000;
289
- backdrop-filter: blur(4px);
290
- }
291
- .dsh-goal-modal {
292
- background: var(--dsw-alias-bg-layer-3);
293
- border: 1px solid var(--dsw-alias-border-l2);
294
- border-radius: 12px;
295
- width: 520px;
296
- max-width: 90vw;
297
- max-height: 80vh;
298
- display: flex;
299
- flex-direction: column;
300
- box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
301
- }
302
- .dsh-goal-modal-head {
303
- padding: 14px 18px;
304
- border-bottom: 1px solid var(--dsw-alias-border-l2);
305
- display: flex;
306
- align-items: center;
307
- justify-content: space-between;
308
- }
309
- .dsh-goal-modal-title {
310
- font-size: 15px;
311
- font-weight: 600;
312
- color: var(--dsw-alias-label-primary);
313
- }
314
- .dsh-goal-modal-body {
315
- padding: 16px 18px;
316
- overflow-y: auto;
317
- display: flex;
318
- flex-direction: column;
319
- gap: 12px;
320
- }
321
- .dsh-goal-milestone-item {
322
- display: flex;
323
- align-items: flex-start;
324
- gap: 10px;
325
- padding: 8px 10px;
326
- border-radius: 8px;
327
- background: var(--dsw-alias-bg-layer-2);
328
- font-size: 13px;
329
- }
330
- .dsh-goal-modal-foot {
331
- padding: 12px 18px;
332
- border-top: 1px solid var(--dsw-alias-border-l2);
333
- display: flex;
334
- justify-content: flex-end;
335
- gap: 8px;
336
- }
337
-
338
- /* Settings Card */
339
- .dsh-goal-card {
340
- border: 1px solid var(--dsw-alias-border-l2);
341
- background: var(--dsw-alias-bg-layer-3);
342
- border-radius: 12px;
343
- list-style: none;
344
- margin-bottom: 10px;
345
- }
346
- .dsh-goal-card-head {
347
- appearance: none;
348
- width: 100%;
349
- font: inherit;
350
- color: inherit;
351
- text-align: left;
352
- cursor: pointer;
353
- background: 0 0;
354
- border: 0;
355
- border-radius: 12px;
356
- display: flex;
357
- align-items: center;
358
- gap: 12px;
359
- padding: 14px 16px;
360
- }
361
- .dsh-goal-card-title {
362
- color: var(--dsw-alias-label-primary);
363
- font-size: 15px;
364
- font-weight: 600;
365
- line-height: 1.4;
366
- }
367
- .dsh-goal-card-sub {
368
- color: var(--dsw-alias-label-secondary);
369
- font-size: 13px;
370
- }
371
- .dsh-goal-card-body {
372
- border-top: 1px solid var(--dsw-alias-border-l2);
373
- margin: 0 16px;
374
- padding-bottom: 12px;
375
- }
376
- .dsh-goal-card-field {
377
- display: flex;
378
- flex-direction: column;
379
- gap: 6px;
380
- padding: 12px 0;
381
- }
382
- .dsh-goal-field-label-row {
383
- display: flex;
384
- align-items: center;
385
- justify-content: space-between;
386
- font-size: 13px;
387
- font-weight: 500;
388
- }
389
- .dsh-goal-field-meta {
390
- display: flex;
391
- align-items: center;
392
- gap: 6px;
393
- font-size: 11px;
394
- color: var(--dsw-alias-label-tertiary);
395
- }
396
- .dsh-goal-override-tag {
397
- font-size: 11px;
398
- padding: 1px 6px;
399
- border-radius: 4px;
400
- background: var(--dsw-alias-bg-layer-2);
401
- color: var(--dsw-alias-label-secondary);
402
- }
403
- .dsh-goal-card-input {
404
- height: 34px;
405
- border: 1px solid var(--dsw-alias-border-l2);
406
- background: var(--dsw-alias-bg-layer-3);
407
- color: var(--dsw-alias-label-primary);
408
- border-radius: 8px;
409
- padding: 0 12px;
410
- font-size: 13px;
411
- }
412
- .dsh-goal-card-input[aria-invalid="true"] {
413
- border-color: var(--dsw-alias-status-danger);
414
- }
415
- .dsh-goal-chev {
416
- margin-left: auto;
417
- flex: none;
418
- color: var(--dsw-alias-label-tertiary);
419
- transition: transform .16s;
420
- }
421
- .dsh-goal-chev-open {
422
- transform: rotate(180deg);
423
- }
424
- .dsh-goal-check-row {
425
- display: flex;
426
- align-items: center;
427
- justify-content: space-between;
428
- padding: 8px 0;
429
- }
430
- .dsh-goal-check {
431
- display: flex;
432
- align-items: center;
433
- gap: 8px;
434
- font-size: 13px;
435
- color: var(--dsw-alias-label-primary);
436
- cursor: pointer;
437
- }
438
- .dsh-goal-foot {
439
- border-top: 1px solid var(--dsw-alias-border-l2);
440
- display: flex;
441
- justify-content: flex-end;
442
- align-items: center;
443
- gap: 8px;
444
- padding: 12px 0 4px;
445
- }
446
- .dsh-goal-foot-note {
447
- color: var(--dsw-alias-status-danger);
448
- font-size: 12px;
449
- margin-right: auto;
450
- }
451
- .dsh-goal-btn-inline-reset {
452
- background: transparent;
453
- border: 0;
454
- cursor: pointer;
455
- padding: 2px 4px;
456
- display: flex;
457
- align-items: center;
458
- gap: 4px;
459
- border-radius: 4px;
460
- color: var(--dsw-alias-label-tertiary);
461
- font-size: 11px;
462
- }
463
- .dsh-goal-btn-inline-reset:hover {
464
- color: var(--dsw-alias-label-primary);
465
- background: var(--dsw-alias-bg-layer-2);
466
- }
467
- .dsh-goal-discard {
468
- appearance: none;
469
- font: inherit;
470
- cursor: pointer;
471
- border: 1px solid var(--dsw-alias-border-l2);
472
- border-radius: 8px;
473
- padding: 5px 14px;
474
- font-size: 13px;
475
- background: transparent;
476
- color: var(--dsw-alias-label-secondary);
477
- transition: all 0.15s ease;
478
- }
479
- .dsh-goal-discard:hover:not(:disabled) {
480
- color: var(--dsw-alias-label-primary);
481
- background: var(--dsw-alias-bg-layer-2);
482
- }
483
- .dsh-goal-discard:disabled {
484
- opacity: 0.4;
485
- cursor: default;
486
- }
487
- .dsh-goal-save {
488
- appearance: none;
489
- font: inherit;
490
- cursor: pointer;
491
- border: 1px solid transparent;
492
- border-radius: 8px;
493
- padding: 5px 14px;
494
- font-size: 13px;
495
- background: var(--dsw-alias-label-primary);
496
- color: var(--dsw-alias-bg-layer-3);
497
- font-weight: 500;
498
- transition: opacity 0.15s ease;
499
- }
500
- .dsh-goal-save:disabled {
501
- opacity: 0.4;
502
- cursor: default;
503
- }
504
- `;
505
-
506
- function injectStylesOnce() {
507
- if (typeof document === 'undefined') return;
508
- if (document.getElementById('dsh-goal-styles')) return;
509
- const style = document.createElement('style');
510
- style.id = 'dsh-goal-styles';
511
- style.textContent = CSS_STYLES;
512
- document.head.appendChild(style);
513
- }
514
-
515
- // --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
516
- function GoalDetailsModal({ state, onClose, onAction, t }) {
517
- if (!state) return null;
518
-
519
- const milestones = state.milestones || [];
520
- const isCompleted = state.state === 'COMPLETED';
521
-
522
- return React.createElement(
523
- 'div',
524
- {
525
- className: 'dsh-goal-modal-overlay',
526
- onClick: (e) => {
527
- if (e.target === e.currentTarget) onClose();
528
- },
529
- },
530
- React.createElement(
531
- 'div',
532
- { className: 'dsh-goal-modal' },
533
- React.createElement(
534
- 'div',
535
- { className: 'dsh-goal-modal-head' },
536
- React.createElement(
537
- 'div',
538
- { style: { display: 'flex', alignItems: 'center', gap: 8 } },
539
- isCompleted ? React.createElement(IconCheck, { size: 18 }) : React.createElement(IconTarget, { size: 18 }),
540
- React.createElement(
541
- 'span',
542
- { className: 'dsh-goal-modal-title' },
543
- isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
544
- ),
545
- ),
546
- React.createElement(
547
- 'button',
548
- { className: 'dsh-goal-btn', onClick: onClose, title: t('close') || 'Закрыть' },
549
- '✕',
550
- ),
551
- ),
552
- React.createElement(
553
- 'div',
554
- { className: 'dsh-goal-modal-body' },
555
- React.createElement(
556
- 'div',
557
- null,
558
- React.createElement('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 4 } }, state.title),
559
- state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 4 } }, state.description) : null,
560
- ),
561
- React.createElement(
562
- 'div',
563
- { style: { display: 'flex', justifyContent: 'flex-end', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
564
- React.createElement('span', null, `${t('time') || 'Время'}: ${state.formattedElapsed || '0s'}`),
565
- ),
566
- React.createElement(
567
- 'div',
568
- { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
569
- React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'План работ:'),
570
- milestones.length === 0
571
- ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
572
- : milestones.map((m, i) =>
573
- React.createElement(
574
- 'div',
575
- { key: m.id || i, className: 'dsh-goal-milestone-item' },
576
- React.createElement('span', { style: { fontSize: 14 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
577
- React.createElement(
578
- 'div',
579
- { style: { flex: 1, minWidth: 0 } },
580
- React.createElement(
581
- 'div',
582
- {
583
- style: {
584
- textDecoration: m.status === 'completed' ? 'line-through' : 'none',
585
- color: m.status === 'completed' ? 'var(--dsw-alias-label-tertiary)' : 'inherit',
586
- fontWeight: m.status === 'in_progress' ? 600 : 400,
587
- },
588
- },
589
- `${i + 1}. ${m.title}`,
590
- ),
591
- m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
592
- ),
593
- ),
594
- ),
595
- ),
596
- ),
597
- React.createElement(
598
- 'div',
599
- { className: 'dsh-goal-modal-foot' },
600
- isCompleted
601
- ? React.createElement(
602
- 'button',
603
- {
604
- className: 'dsh-goal-btn danger',
605
- onClick: () => {
606
- onAction('clear');
607
- onClose();
608
- },
609
- style: { padding: '6px 12px', fontSize: 13 },
610
- },
611
- t('closeBanner') || 'Закрыть плашку цели',
612
- )
613
- : null,
614
- React.createElement(
615
- 'button',
616
- {
617
- className: 'dsh-goal-save',
618
- onClick: onClose,
619
- style: { padding: '6px 16px' },
620
- },
621
- t('close') || 'Закрыть',
622
- ),
623
- ),
624
- ),
625
- );
626
- }
627
-
628
- // --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
629
- function GoalTopBanner(props) {
630
- const { ctx } = props || {};
631
- const [state, setState] = useState(null);
632
- const [isModalOpen, setIsModalOpen] = useState(false);
633
- const [now, setNow] = useState(() => Date.now());
634
-
635
- const t = (key) => {
636
- if (ctx?.locale?.getSnapshot) {
637
- const snap = ctx.locale.getSnapshot();
638
- const active = snap?.active || 'ru';
639
- return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
640
- }
641
- return LOCALES.ru[key] || LOCALES.en[key] || key;
642
- };
643
-
644
- useEffect(() => {
645
- injectStylesOnce();
646
- }, []);
647
-
648
- // Локальный секундный интервал для плавного и бесперебойного тикания таймера
649
- useEffect(() => {
650
- const ticker = setInterval(() => {
651
- setNow(Date.now());
652
- }, 1000);
653
- return () => clearInterval(ticker);
654
- }, []);
655
-
656
- const fetchState = useCallback(async () => {
657
- try {
658
- const res = await fetch('/dsh-goal/state');
659
- if (res.ok) {
660
- const data = await res.json();
661
- setState(data);
662
- }
663
- } catch (_) {}
664
- }, []);
665
-
666
- useEffect(() => {
667
- fetchState();
668
- const timer = setInterval(fetchState, 1500);
669
- return () => clearInterval(timer);
670
- }, [fetchState]);
671
-
672
- const handleAction = async (action, extra = {}) => {
673
- try {
674
- const res = await fetch('/dsh-goal/action', {
675
- method: 'POST',
676
- headers: { 'Content-Type': 'application/json' },
677
- body: JSON.stringify({ action, ...extra }),
678
- });
679
- if (res.ok) {
680
- const data = await res.json();
681
- if (data.state) setState(data.state);
682
- }
683
- } catch (_) {}
684
- };
685
-
686
- if (!state || !state.hasActiveGoal) {
687
- return null;
688
- }
689
-
690
- const isPaused = state.state === 'PAUSED';
691
- const isCompleted = state.state === 'COMPLETED';
692
-
693
- const formatLiveElapsed = () => {
694
- if (!state) return '0s';
695
- if (!state.startedAt) return state.formattedElapsed || '0s';
696
- const endTime = state.completedAt || (state.pausedAt || now);
697
- const elapsedSec = Math.max(0, Math.floor((endTime - state.startedAt - (state.totalPausedDurationMs || 0)) / 1000));
698
- return formatElapsed(elapsedSec);
699
- };
700
-
701
- const liveElapsedStr = formatLiveElapsed();
702
-
703
- return React.createElement(
704
- React.Fragment,
705
- null,
706
- React.createElement(
707
- 'div',
708
- { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
709
- React.createElement(
710
- 'div',
711
- { className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
712
- React.createElement(
713
- 'div',
714
- { className: 'dsh-goal-left' },
715
- React.createElement(
716
- 'span',
717
- { className: 'dsh-goal-icon' },
718
- isCompleted ? React.createElement(IconCheck, { size: 16 }) : React.createElement(IconTarget, { size: 15 }),
719
- ),
720
- React.createElement(
721
- 'span',
722
- { className: `dsh-goal-label${isCompleted ? ' completed' : ''}` },
723
- isCompleted ? (t('goalCompleted') || 'Цель выполнена') : (t('goalLabel') || 'Текущая цель'),
724
- ),
725
- React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
726
- ),
727
- React.createElement(
728
- 'div',
729
- { className: 'dsh-goal-right' },
730
- React.createElement(
731
- 'span',
732
- { className: 'dsh-goal-time' },
733
- `⏱ ${liveElapsedStr}`,
734
- ),
735
- !isCompleted
736
- ? React.createElement(
737
- 'button',
738
- {
739
- className: 'dsh-goal-btn',
740
- title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
741
- onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
742
- },
743
- isPaused ? React.createElement(IconPlay, { size: 14 }) : React.createElement(IconPause, { size: 14 }),
744
- )
745
- : null,
746
- React.createElement(
747
- 'button',
748
- {
749
- className: 'dsh-goal-btn',
750
- title: t('details') || 'Развернуть детали цели',
751
- onClick: () => setIsModalOpen(true),
752
- },
753
- React.createElement(IconExpand, { size: 14 }),
754
- ),
755
- isCompleted
756
- ? React.createElement(
757
- 'button',
758
- {
759
- className: 'dsh-goal-btn close',
760
- title: t('closeBanner') || 'Закрыть плашку цели',
761
- onClick: () => handleAction('clear'),
762
- },
763
- '✕',
764
- )
765
- : null,
766
- ),
767
- ),
768
- ),
769
- isModalOpen
770
- ? React.createElement(GoalDetailsModal, {
771
- state,
772
- onClose: () => setIsModalOpen(false),
773
- onAction: handleAction,
774
- t,
775
- })
776
- : null,
777
- );
778
- }
779
-
780
- // --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
781
- // Архитектурное решение (Issue #23):
782
- // Чистая логика состояния формы настроек вынесена в `lib/card-form-state.js` для запуска
783
- // быстрых изолированных unit-тестов через `node --test`. В `lib/client.js` эти функции
784
- // продублированы встроенным образом, поскольку клиентский бандл загружается браузерным
785
- // загрузчиком DSH (`window.__ModuleLoader__`) как самодостаточный автономный скрипт
786
- // без этапа сборки (bundler / webpack) и не может динамически импортировать локальные файлы.
787
- const DEFAULT_SETTINGS = {
788
- maxIterations: 25,
789
- autoDrive: true,
790
- enableSound: true,
791
- };
792
-
793
- function parseNumberField(raw) {
794
- if (raw === '' || raw === null || raw === undefined) {
795
- return undefined;
796
- }
797
- const n = Number(raw);
798
- if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
799
- return NaN;
800
- }
801
- return n;
802
- }
803
-
804
- function getFieldStatus(field, draftValue, snap) {
805
- const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
806
- const userVal = snap?.user?.[field];
807
- const effectiveVal = snap?.value?.[field] ?? baseVal;
808
-
809
- const isOverridden = userVal !== undefined && userVal !== null;
810
- const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
811
-
812
- let invalid = false;
813
- let isDirty = false;
814
-
815
- if (field === 'maxIterations') {
816
- if (draftValue !== undefined) {
817
- if (draftValue === '') {
818
- invalid = false;
819
- isDirty = userVal !== undefined;
820
- } else {
821
- const parsed = parseNumberField(draftValue);
822
- if (Number.isNaN(parsed)) {
823
- invalid = true;
824
- isDirty = true;
825
- } else {
826
- isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
827
- }
828
- }
829
- }
830
- } else {
831
- if (draftValue !== undefined) {
832
- isDirty = draftValue !== (userVal !== undefined ? userVal : baseVal);
833
- }
834
- }
835
-
836
- return {
837
- value: currentVal,
838
- baseValue: baseVal,
839
- userValue: userVal,
840
- isOverridden,
841
- isDirty,
842
- invalid,
843
- };
844
- }
845
-
846
- function computeSavePlan(draft, snap) {
847
- if (!draft) return [];
848
-
849
- const writes = [];
850
- const fields = ['maxIterations', 'autoDrive', 'enableSound'];
851
-
852
- for (const f of fields) {
853
- if (draft[f] === undefined) continue;
854
-
855
- const status = getFieldStatus(f, draft[f], snap);
856
- if (status.invalid) continue;
857
-
858
- if (status.isDirty) {
859
- if (f === 'maxIterations' && draft[f] === '') {
860
- writes.push([f, undefined]);
861
- } else if (f === 'maxIterations') {
862
- writes.push([f, parseNumberField(draft[f])]);
863
- } else {
864
- writes.push([f, Boolean(draft[f])]);
865
- }
866
- }
867
- }
868
-
869
- return writes;
870
- }
871
-
872
- // --- КАРТОЧКА НАСТРОЕК (SETTINGS CARD) ---
873
- function GoalSettingsCard(props) {
874
- const { scope, ctx } = props || {};
875
- const [isOpen, setIsOpen] = useState(false);
876
- const [draft, setDraft] = useState(null);
877
- const [saving, setSaving] = useState(false);
878
- const [failed, setFailed] = useState(false);
879
-
880
- const [snap, setSnap] = useState(() => (scope ? scope.getSnapshot() : { value: DEFAULT_SETTINGS, base: DEFAULT_SETTINGS, user: {} }));
881
-
882
- useEffect(() => {
883
- injectStylesOnce();
884
- }, []);
885
-
886
- useEffect(() => {
887
- if (!scope || typeof scope.subscribe !== 'function') return;
888
- return scope.subscribe((next) => {
889
- setSnap(next || scope.getSnapshot());
890
- });
891
- }, [scope]);
892
-
893
- const t = (key) => {
894
- if (ctx?.locale?.getSnapshot) {
895
- const snapLoc = ctx.locale.getSnapshot();
896
- const active = snapLoc?.active || 'ru';
897
- return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
898
- }
899
- return LOCALES.ru[key] || LOCALES.en[key] || key;
900
- };
901
-
902
- const status = (snap && snap.status) || 'ready';
903
- const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
904
- const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
905
- const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
906
-
907
- const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
908
- const invalid = maxIterStatus.invalid;
909
- const disabled = !scope || saving || snap?.writable === false;
910
-
911
- const edit = (field, val) => {
912
- setFailed(false);
913
- setDraft((prev) => ({
914
- maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
915
- autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
916
- enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
917
- [field]: val,
918
- }));
919
- };
920
-
921
- const resetFieldToDefault = (field) => {
922
- if (field === 'maxIterations') {
923
- edit(field, '');
924
- } else {
925
- edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
926
- }
927
- };
928
-
929
- const save = async () => {
930
- if (!scope || !dirty || invalid) return;
931
- setSaving(true);
932
- setFailed(false);
933
-
934
- const writes = computeSavePlan(draft, snap);
935
- let landed = true;
936
-
937
- for (const [field, fieldValue] of writes) {
938
- try {
939
- await scope.set(field, fieldValue);
940
- } catch (err) {
941
- console.error('[dsh-goal] failed to save setting', field, err);
942
- landed = false;
943
- }
944
- }
945
-
946
- const fresh = scope.getSnapshot();
947
- const user = fresh.user || {};
948
- for (const [field, fieldValue] of writes) {
949
- if (fieldValue === undefined) {
950
- if (user[field] !== undefined) landed = false;
951
- } else {
952
- if (user[field] !== fieldValue) landed = false;
953
- }
954
- }
955
-
956
- setSnap(fresh);
957
- setSaving(false);
958
- if (landed) {
959
- setDraft(null);
960
- } else {
961
- setFailed(true);
962
- }
963
- };
964
-
965
- const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
966
-
967
- return React.createElement(
968
- 'li',
969
- { className: 'dsh-goal-card' },
970
- React.createElement(
971
- 'button',
972
- {
973
- className: 'dsh-goal-card-head',
974
- onClick: () => setIsOpen(!isOpen),
975
- 'aria-expanded': isOpen,
976
- },
977
- React.createElement(
978
- 'div',
979
- { style: { flex: 1 } },
980
- React.createElement('div', { className: 'dsh-goal-card-title' }, t('pluginTitle') || 'Цели и автономный режим (Goal Mode)'),
981
- React.createElement('div', { className: 'dsh-goal-card-sub' }, t('pluginDesc') || 'Верхняя закреплённая панель цели, декомпозиция вех и авто-драйв'),
982
- ),
983
- React.createElement(Chevron, { className: `dsh-goal-chev ${isOpen ? 'dsh-goal-chev-open' : ''}` }),
984
- ),
985
- isOpen
986
- ? React.createElement(
987
- 'div',
988
- { className: 'dsh-goal-card-body' },
989
- status === 'loading'
990
- ? React.createElement('p', { className: 'dsh-goal-field-hint', style: { padding: '12px 0' } }, t('loading') || 'Загрузка настроек…')
991
- : status !== 'ready'
992
- ? React.createElement('p', { className: 'dsh-goal-foot-note', style: { padding: '12px 0' } }, t('unavailable') || 'Настройки недоступны')
993
- : React.createElement(
994
- React.Fragment,
995
- null,
996
- React.createElement(
997
- 'div',
998
- { className: 'dsh-goal-card-field' },
999
- React.createElement(
1000
- 'div',
1001
- { className: 'dsh-goal-field-label-row' },
1002
- React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1003
- React.createElement(
1004
- 'div',
1005
- { className: 'dsh-goal-field-meta' },
1006
- maxIterStatus.isOverridden
1007
- ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1008
- : null,
1009
- maxIterStatus.isOverridden
1010
- ? React.createElement(
1011
- 'button',
1012
- {
1013
- type: 'button',
1014
- className: 'dsh-goal-btn-inline-reset',
1015
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1016
- onClick: () => resetFieldToDefault('maxIterations'),
1017
- },
1018
- React.createElement(IconRotateCcw, { size: 12 }),
1019
- t('resetField') || 'По умолчанию',
1020
- )
1021
- : null,
1022
- ),
1023
- ),
1024
- React.createElement('input', {
1025
- id: 'dsh-goal-max-iter',
1026
- type: 'number',
1027
- min: 1,
1028
- placeholder: String(maxIterStatus.baseValue),
1029
- className: 'dsh-goal-card-input',
1030
- value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1031
- disabled,
1032
- 'aria-invalid': invalid,
1033
- onChange: (e) => edit('maxIterations', e.target.value),
1034
- }),
1035
- ),
1036
- React.createElement(
1037
- 'div',
1038
- { className: 'dsh-goal-check-row' },
1039
- React.createElement(
1040
- 'label',
1041
- { className: 'dsh-goal-check' },
1042
- React.createElement('input', {
1043
- type: 'checkbox',
1044
- checked: autoDriveStatus.value === true,
1045
- disabled,
1046
- onChange: (e) => edit('autoDrive', e.target.checked),
1047
- }),
1048
- t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1049
- ),
1050
- autoDriveStatus.isOverridden
1051
- ? React.createElement(
1052
- 'button',
1053
- {
1054
- type: 'button',
1055
- className: 'dsh-goal-btn-inline-reset',
1056
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1057
- onClick: () => resetFieldToDefault('autoDrive'),
1058
- },
1059
- React.createElement(IconRotateCcw, { size: 12 }),
1060
- )
1061
- : null,
1062
- ),
1063
- React.createElement(
1064
- 'div',
1065
- { className: 'dsh-goal-check-row' },
1066
- React.createElement(
1067
- 'label',
1068
- { className: 'dsh-goal-check' },
1069
- React.createElement('input', {
1070
- type: 'checkbox',
1071
- checked: enableSoundStatus.value === true,
1072
- disabled,
1073
- onChange: (e) => edit('enableSound', e.target.checked),
1074
- }),
1075
- t('soundLabel') || 'Звук по завершении цели',
1076
- ),
1077
- enableSoundStatus.isOverridden
1078
- ? React.createElement(
1079
- 'button',
1080
- {
1081
- type: 'button',
1082
- className: 'dsh-goal-btn-inline-reset',
1083
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1084
- onClick: () => resetFieldToDefault('enableSound'),
1085
- },
1086
- React.createElement(IconRotateCcw, { size: 12 }),
1087
- )
1088
- : null,
1089
- ),
1090
- React.createElement(
1091
- 'div',
1092
- { className: 'dsh-goal-foot' },
1093
- failed
1094
- ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1095
- : null,
1096
- React.createElement(
1097
- 'button',
1098
- {
1099
- className: 'dsh-goal-discard',
1100
- disabled: !dirty || disabled,
1101
- onClick: () => {
1102
- setFailed(false);
1103
- setDraft(null);
1104
- },
1105
- },
1106
- t('discard') || 'Отменить правки',
1107
- ),
1108
- React.createElement(
1109
- 'button',
1110
- { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1111
- saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1112
- ),
1113
- ),
1114
- )
1115
- )
1116
- : null,
1117
- );
1118
- }
1119
-
1120
- // --- СЛОВАРИ ЛОКАЛИЗАЦИИ ---
1121
- const LOCALES = {
1122
- ru: {
1123
- goalLabel: 'Текущая цель',
1124
- goalCompleted: 'Цель выполнена',
1125
- clearGoal: 'Очистить цель',
1126
- closeBanner: 'Закрыть плашку цели',
1127
- pause: 'Приостановить',
1128
- resume: 'Возобновить',
1129
- details: 'Развернуть детали цели',
1130
- modalTitle: 'План и статус цели',
1131
- modalTitleCompleted: 'Цель выполнена: план и результаты',
1132
- time: 'Время',
1133
- milestones: 'План работ:',
1134
- noMilestones: 'Агент формирует план работ...',
1135
- close: 'Закрыть',
1136
- pluginTitle: 'Цели и автономный режим (Goal Mode)',
1137
- pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
1138
- maxIterLabel: 'Максимум итераций (Safety Limit):',
1139
- autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
1140
- soundLabel: 'Звук по завершении цели',
1141
- save: 'Сохранить',
1142
- saving: 'Сохранение…',
1143
- discard: 'Отменить правки',
1144
- resetField: 'По умолчанию',
1145
- overridden: 'изменено',
1146
- saveFailed: 'Не сохранено — исправьте и повторите',
1147
- loading: 'Загрузка настроек…',
1148
- unavailable: 'Настройки недоступны',
1149
- },
1150
- en: {
1151
- goalLabel: 'Current goal',
1152
- goalCompleted: 'Goal completed',
1153
- clearGoal: 'Clear goal',
1154
- closeBanner: 'Close goal banner',
1155
- pause: 'Pause',
1156
- resume: 'Resume',
1157
- details: 'Expand Goal Details',
1158
- modalTitle: 'Goal Plan & Status',
1159
- modalTitleCompleted: 'Goal Completed: Plan & Results',
1160
- time: 'Time',
1161
- milestones: 'Plan of work:',
1162
- noMilestones: 'Agent is preparing the plan of work...',
1163
- close: 'Close',
1164
- pluginTitle: 'Goal Mode & Autonomous Loop',
1165
- pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
1166
- maxIterLabel: 'Max iterations (Safety Limit):',
1167
- autoDriveLabel: 'Auto-drive: keep the loop running automatically',
1168
- soundLabel: 'Sound when a goal completes',
1169
- save: 'Save',
1170
- saving: 'Saving…',
1171
- discard: 'Discard changes',
1172
- resetField: 'Default',
1173
- overridden: 'overridden',
1174
- saveFailed: 'Not saved — fix the values and try again',
1175
- loading: 'Loading settings…',
1176
- unavailable: 'Settings unavailable',
1177
- },
1178
- };
1179
-
1180
- module.exports.inject = ['slots', 'locale', 'settingsScope'];
1181
- module.exports.apply = function apply(ctx) {
1182
- // Регистрация локализации
1183
- try {
1184
- ctx.locale?.register?.(NS, LOCALES);
1185
- } catch (_) {}
1186
-
1187
- // Пространство настроек плагина
1188
- const settingsScope =
1189
- ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
1190
- ? ctx.settingsScope.bind({ namespace: NS })
1191
- : null;
1192
-
1193
- const registerSlotSafe = (name, entry, comp) => {
1194
- try {
1195
- if (typeof ctx.slots?.inject === 'function') {
1196
- ctx.slots.inject(name, () => {
1197
- try {
1198
- return ctx.slots.register(entry, comp);
1199
- } catch (_) {}
1200
- });
1201
- } else if (typeof ctx.slots?.register === 'function') {
1202
- ctx.slots.register(entry, comp);
1203
- }
1204
- } catch (_) {}
1205
- };
1206
-
1207
- // 1. Регистрация виджета цели над полем ввода (composer dock)
1208
- registerSlotSafe(
1209
- 'conversation.input.dock',
1210
- {
1211
- name: 'conversation.input.dock',
1212
- id: '@goodandready/dsh-goal',
1213
- order: 10,
1214
- locale: NS,
1215
- inject: () => ({ ctx }),
1216
- },
1217
- GoalTopBanner,
1218
- );
1219
-
1220
- // 2. Регистрация карточки настроек
1221
- registerSlotSafe(
1222
- 'settings.plugin.item',
1223
- {
1224
- name: 'settings.plugin.item',
1225
- key: NS,
1226
- locale: NS,
1227
- inject: () => ({ ctx, scope: settingsScope }),
1228
- },
1229
- GoalSettingsCard,
1230
- );
1231
- };
1232
-
1233
- return module.exports;
1234
- },
1235
- });
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 } = 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
+ style: { color: 'var(--dsw-alias-status-success)' },
112
+ className,
113
+ },
114
+ React.createElement('polyline', { points: '20 6 9 17 4 12' }),
115
+ );
116
+ }
117
+
118
+ function IconExpand({ size = 14, className = '' }) {
119
+ return React.createElement(
120
+ 'svg',
121
+ {
122
+ width: size,
123
+ height: size,
124
+ viewBox: '0 0 24 24',
125
+ fill: 'none',
126
+ stroke: 'currentColor',
127
+ strokeWidth: 1.8,
128
+ strokeLinecap: 'round',
129
+ strokeLinejoin: 'round',
130
+ className,
131
+ },
132
+ React.createElement('polyline', { points: '15 3 21 3 21 9' }),
133
+ React.createElement('polyline', { points: '9 21 3 21 3 15' }),
134
+ React.createElement('line', { x1: 21, y1: 3, x2: 14, y2: 10 }),
135
+ React.createElement('line', { x1: 3, y1: 21, x2: 10, y2: 14 }),
136
+ );
137
+ }
138
+
139
+ function IconRotateCcw({ size = 13, className = '' }) {
140
+ return React.createElement(
141
+ 'svg',
142
+ {
143
+ width: size,
144
+ height: size,
145
+ viewBox: '0 0 24 24',
146
+ fill: 'none',
147
+ stroke: 'currentColor',
148
+ strokeWidth: 2,
149
+ strokeLinecap: 'round',
150
+ strokeLinejoin: 'round',
151
+ className,
152
+ },
153
+ React.createElement('polyline', { points: '1 4 1 10 7 10' }),
154
+ React.createElement('path', { d: 'M3.51 15a9 9 0 1 0 2.13-9.36L1 10' }),
155
+ );
156
+ }
157
+
158
+ function IconChevronDown({ size = 14, className = '' }) {
159
+ return React.createElement(
160
+ 'svg',
161
+ {
162
+ width: size,
163
+ height: size,
164
+ viewBox: '0 0 24 24',
165
+ fill: 'none',
166
+ stroke: 'currentColor',
167
+ strokeWidth: 2,
168
+ strokeLinecap: 'round',
169
+ strokeLinejoin: 'round',
170
+ className,
171
+ },
172
+ React.createElement('polyline', { points: '6 9 12 15 18 9' }),
173
+ );
174
+ }
175
+
176
+ // --- СТИЛИ И CSS ПРАВИЛА (ЧИСТЫЕ DSH ТОКЕНЫ БЕЗ ХАРДКОД-ФОЛЛБЕКОВ) ---
177
+ const CSS_STYLES = `
178
+ .dsh-goal-dock {
179
+ box-sizing: border-box;
180
+ width: calc(100% - var(--dsh-composer-side-clearance, 0px) * 2 - var(--dsh-composer-dock-inset, 0px) * 4);
181
+ max-width: calc(var(--dsh-composer-card-max-width, 768px) - var(--dsh-composer-dock-inset, 0px) * 4);
182
+ margin: 0 auto 8px auto;
183
+ display: flex;
184
+ justify-content: center;
185
+ }
186
+ .dsh-goal-banner {
187
+ box-sizing: border-box;
188
+ width: 100%;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: space-between;
192
+ background: var(--dsw-alias-bg-layer-2);
193
+ border: 1px solid var(--dsw-alias-border-l2);
194
+ border-radius: 12px;
195
+ padding: 6px 14px;
196
+ font-family: inherit;
197
+ font-size: 13px;
198
+ color: var(--dsw-alias-label-primary);
199
+ user-select: none;
200
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
201
+ transition: all 0.2s ease;
202
+ }
203
+ .dsh-goal-left {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ min-width: 0;
208
+ flex: 1;
209
+ }
210
+ .dsh-goal-icon {
211
+ color: var(--dsw-alias-label-secondary);
212
+ flex-shrink: 0;
213
+ display: flex;
214
+ align-items: center;
215
+ }
216
+ .dsh-goal-label {
217
+ font-weight: 600;
218
+ color: var(--dsw-alias-label-primary);
219
+ white-space: nowrap;
220
+ }
221
+ .dsh-goal-label.completed {
222
+ color: var(--dsw-alias-status-success);
223
+ }
224
+ .dsh-goal-title {
225
+ color: var(--dsw-alias-label-secondary);
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ white-space: nowrap;
229
+ font-weight: 400;
230
+ flex: 1;
231
+ min-width: 0;
232
+ }
233
+ .dsh-goal-time {
234
+ color: var(--dsw-alias-label-secondary);
235
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
236
+ font-size: 12px;
237
+ font-weight: 500;
238
+ white-space: nowrap;
239
+ display: inline-flex;
240
+ align-items: center;
241
+ gap: 4px;
242
+ flex-shrink: 0;
243
+ }
244
+ .dsh-goal-right {
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 8px;
248
+ margin-left: 12px;
249
+ flex-shrink: 0;
250
+ }
251
+ .dsh-goal-btn {
252
+ background: transparent;
253
+ border: 0;
254
+ padding: 4px;
255
+ margin: 0;
256
+ color: var(--dsw-alias-label-tertiary);
257
+ cursor: pointer;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ border-radius: 6px;
262
+ transition: all 0.15s ease;
263
+ }
264
+ .dsh-goal-btn:hover {
265
+ color: var(--dsw-alias-label-primary);
266
+ background: var(--dsw-alias-bg-layer-3);
267
+ }
268
+ .dsh-goal-btn.close {
269
+ font-size: 13px;
270
+ font-weight: bold;
271
+ padding: 3px 6px;
272
+ }
273
+ .dsh-goal-btn.close:hover {
274
+ color: var(--dsw-alias-status-danger);
275
+ }
276
+ .dsh-goal-btn.danger:hover {
277
+ color: var(--dsw-alias-status-danger);
278
+ }
279
+
280
+ /* Modal Details */
281
+ .dsh-goal-modal-overlay {
282
+ position: fixed;
283
+ inset: 0;
284
+ background: rgba(0, 0, 0, 0.6);
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ z-index: 10000;
289
+ backdrop-filter: blur(4px);
290
+ }
291
+ .dsh-goal-modal {
292
+ background: var(--dsw-alias-bg-layer-3);
293
+ border: 1px solid var(--dsw-alias-border-l2);
294
+ border-radius: 12px;
295
+ width: 520px;
296
+ max-width: 90vw;
297
+ max-height: 80vh;
298
+ display: flex;
299
+ flex-direction: column;
300
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
301
+ }
302
+ .dsh-goal-modal-head {
303
+ padding: 14px 18px;
304
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: space-between;
308
+ }
309
+ .dsh-goal-modal-title {
310
+ font-size: 15px;
311
+ font-weight: 600;
312
+ color: var(--dsw-alias-label-primary);
313
+ }
314
+ .dsh-goal-modal-body {
315
+ padding: 16px 18px;
316
+ overflow-y: auto;
317
+ display: flex;
318
+ flex-direction: column;
319
+ gap: 12px;
320
+ }
321
+ .dsh-goal-milestone-item {
322
+ display: flex;
323
+ align-items: flex-start;
324
+ gap: 10px;
325
+ padding: 8px 10px;
326
+ border-radius: 8px;
327
+ background: var(--dsw-alias-bg-layer-2);
328
+ font-size: 13px;
329
+ }
330
+ .dsh-goal-modal-foot {
331
+ padding: 12px 18px;
332
+ border-top: 1px solid var(--dsw-alias-border-l2);
333
+ display: flex;
334
+ justify-content: flex-end;
335
+ gap: 8px;
336
+ }
337
+
338
+ /* Settings Card */
339
+ .dsh-goal-card {
340
+ border: 1px solid var(--dsw-alias-border-l2);
341
+ background: var(--dsw-alias-bg-layer-3);
342
+ border-radius: 12px;
343
+ list-style: none;
344
+ margin-bottom: 10px;
345
+ }
346
+ .dsh-goal-card-head {
347
+ appearance: none;
348
+ width: 100%;
349
+ font: inherit;
350
+ color: inherit;
351
+ text-align: left;
352
+ cursor: pointer;
353
+ background: 0 0;
354
+ border: 0;
355
+ border-radius: 12px;
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 12px;
359
+ padding: 14px 16px;
360
+ }
361
+ .dsh-goal-card-title {
362
+ color: var(--dsw-alias-label-primary);
363
+ font-size: 15px;
364
+ font-weight: 600;
365
+ line-height: 1.4;
366
+ }
367
+ .dsh-goal-card-sub {
368
+ color: var(--dsw-alias-label-secondary);
369
+ font-size: 13px;
370
+ }
371
+ .dsh-goal-card-body {
372
+ border-top: 1px solid var(--dsw-alias-border-l2);
373
+ margin: 0 16px;
374
+ padding-bottom: 12px;
375
+ }
376
+ .dsh-goal-card-field {
377
+ display: flex;
378
+ flex-direction: column;
379
+ gap: 6px;
380
+ padding: 12px 0;
381
+ }
382
+ .dsh-goal-field-label-row {
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: space-between;
386
+ font-size: 13px;
387
+ font-weight: 500;
388
+ }
389
+ .dsh-goal-field-meta {
390
+ display: flex;
391
+ align-items: center;
392
+ gap: 6px;
393
+ font-size: 11px;
394
+ color: var(--dsw-alias-label-tertiary);
395
+ }
396
+ .dsh-goal-override-tag {
397
+ font-size: 11px;
398
+ padding: 1px 6px;
399
+ border-radius: 4px;
400
+ background: var(--dsw-alias-bg-layer-2);
401
+ color: var(--dsw-alias-label-secondary);
402
+ }
403
+ .dsh-goal-card-input {
404
+ height: 34px;
405
+ border: 1px solid var(--dsw-alias-border-l2);
406
+ background: var(--dsw-alias-bg-layer-3);
407
+ color: var(--dsw-alias-label-primary);
408
+ border-radius: 8px;
409
+ padding: 0 12px;
410
+ font-size: 13px;
411
+ }
412
+ .dsh-goal-card-input[aria-invalid="true"] {
413
+ border-color: var(--dsw-alias-status-danger);
414
+ }
415
+ .dsh-goal-chev {
416
+ margin-left: auto;
417
+ flex: none;
418
+ color: var(--dsw-alias-label-tertiary);
419
+ transition: transform .16s;
420
+ }
421
+ .dsh-goal-chev-open {
422
+ transform: rotate(180deg);
423
+ }
424
+ .dsh-goal-check-row {
425
+ display: flex;
426
+ align-items: center;
427
+ justify-content: space-between;
428
+ padding: 8px 0;
429
+ }
430
+ .dsh-goal-check {
431
+ display: flex;
432
+ align-items: center;
433
+ gap: 8px;
434
+ font-size: 13px;
435
+ color: var(--dsw-alias-label-primary);
436
+ cursor: pointer;
437
+ }
438
+ .dsh-goal-foot {
439
+ border-top: 1px solid var(--dsw-alias-border-l2);
440
+ display: flex;
441
+ justify-content: flex-end;
442
+ align-items: center;
443
+ gap: 8px;
444
+ padding: 12px 0 4px;
445
+ }
446
+ .dsh-goal-foot-note {
447
+ color: var(--dsw-alias-status-danger);
448
+ font-size: 12px;
449
+ margin-right: auto;
450
+ }
451
+ .dsh-goal-btn-inline-reset {
452
+ background: transparent;
453
+ border: 0;
454
+ cursor: pointer;
455
+ padding: 2px 4px;
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 4px;
459
+ border-radius: 4px;
460
+ color: var(--dsw-alias-label-tertiary);
461
+ font-size: 11px;
462
+ }
463
+ .dsh-goal-btn-inline-reset:hover {
464
+ color: var(--dsw-alias-label-primary);
465
+ background: var(--dsw-alias-bg-layer-2);
466
+ }
467
+ .dsh-goal-discard {
468
+ appearance: none;
469
+ font: inherit;
470
+ cursor: pointer;
471
+ border: 1px solid var(--dsw-alias-border-l2);
472
+ border-radius: 8px;
473
+ padding: 5px 14px;
474
+ font-size: 13px;
475
+ background: transparent;
476
+ color: var(--dsw-alias-label-secondary);
477
+ transition: all 0.15s ease;
478
+ }
479
+ .dsh-goal-discard:hover:not(:disabled) {
480
+ color: var(--dsw-alias-label-primary);
481
+ background: var(--dsw-alias-bg-layer-2);
482
+ }
483
+ .dsh-goal-discard:disabled {
484
+ opacity: 0.4;
485
+ cursor: default;
486
+ }
487
+ .dsh-goal-save {
488
+ appearance: none;
489
+ font: inherit;
490
+ cursor: pointer;
491
+ border: 1px solid transparent;
492
+ border-radius: 8px;
493
+ padding: 5px 14px;
494
+ font-size: 13px;
495
+ background: var(--dsw-alias-label-primary);
496
+ color: var(--dsw-alias-bg-layer-3);
497
+ font-weight: 500;
498
+ transition: opacity 0.15s ease;
499
+ }
500
+ .dsh-goal-save:disabled {
501
+ opacity: 0.4;
502
+ cursor: default;
503
+ }
504
+ `;
505
+
506
+ function injectStylesOnce() {
507
+ if (typeof document === 'undefined') return;
508
+ if (document.getElementById('dsh-goal-styles')) return;
509
+ const style = document.createElement('style');
510
+ style.id = 'dsh-goal-styles';
511
+ style.textContent = CSS_STYLES;
512
+ document.head.appendChild(style);
513
+ }
514
+
515
+ // --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
516
+ function GoalDetailsModal({ state, onClose, onAction, t }) {
517
+ if (!state) return null;
518
+
519
+ const milestones = state.milestones || [];
520
+ const isCompleted = state.state === 'COMPLETED';
521
+
522
+ return React.createElement(
523
+ 'div',
524
+ {
525
+ className: 'dsh-goal-modal-overlay',
526
+ onClick: (e) => {
527
+ if (e.target === e.currentTarget) onClose();
528
+ },
529
+ },
530
+ React.createElement(
531
+ 'div',
532
+ { className: 'dsh-goal-modal' },
533
+ React.createElement(
534
+ 'div',
535
+ { className: 'dsh-goal-modal-head' },
536
+ React.createElement(
537
+ 'div',
538
+ { style: { display: 'flex', alignItems: 'center', gap: 8 } },
539
+ isCompleted ? React.createElement(IconCheck, { size: 18 }) : React.createElement(IconTarget, { size: 18 }),
540
+ React.createElement(
541
+ 'span',
542
+ { className: 'dsh-goal-modal-title' },
543
+ isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
544
+ ),
545
+ ),
546
+ React.createElement(
547
+ 'button',
548
+ { className: 'dsh-goal-btn', onClick: onClose, title: t('close') || 'Закрыть' },
549
+ '✕',
550
+ ),
551
+ ),
552
+ React.createElement(
553
+ 'div',
554
+ { className: 'dsh-goal-modal-body' },
555
+ React.createElement(
556
+ 'div',
557
+ null,
558
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 4 } }, state.title),
559
+ state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 4 } }, state.description) : null,
560
+ ),
561
+ React.createElement(
562
+ 'div',
563
+ { style: { display: 'flex', justifyContent: 'flex-end', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
564
+ React.createElement('span', null, `${t('time') || 'Время'}: ${state.formattedElapsed || '0s'}`),
565
+ ),
566
+ React.createElement(
567
+ 'div',
568
+ { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
569
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'План работ:'),
570
+ milestones.length === 0
571
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
572
+ : milestones.map((m, i) =>
573
+ React.createElement(
574
+ 'div',
575
+ { key: m.id || i, className: 'dsh-goal-milestone-item' },
576
+ React.createElement('span', { style: { fontSize: 14 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
577
+ React.createElement(
578
+ 'div',
579
+ { style: { flex: 1, minWidth: 0 } },
580
+ React.createElement(
581
+ 'div',
582
+ {
583
+ style: {
584
+ textDecoration: m.status === 'completed' ? 'line-through' : 'none',
585
+ color: m.status === 'completed' ? 'var(--dsw-alias-label-tertiary)' : 'inherit',
586
+ fontWeight: m.status === 'in_progress' ? 600 : 400,
587
+ },
588
+ },
589
+ `${i + 1}. ${m.title}`,
590
+ ),
591
+ m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
592
+ ),
593
+ ),
594
+ ),
595
+ ),
596
+ ),
597
+ React.createElement(
598
+ 'div',
599
+ { className: 'dsh-goal-modal-foot' },
600
+ isCompleted
601
+ ? React.createElement(
602
+ 'button',
603
+ {
604
+ className: 'dsh-goal-btn danger',
605
+ onClick: () => {
606
+ onAction('clear');
607
+ onClose();
608
+ },
609
+ style: { padding: '6px 12px', fontSize: 13 },
610
+ },
611
+ t('closeBanner') || 'Закрыть плашку цели',
612
+ )
613
+ : null,
614
+ React.createElement(
615
+ 'button',
616
+ {
617
+ className: 'dsh-goal-save',
618
+ onClick: onClose,
619
+ style: { padding: '6px 16px' },
620
+ },
621
+ t('close') || 'Закрыть',
622
+ ),
623
+ ),
624
+ ),
625
+ );
626
+ }
627
+
628
+ function sessionIdOf(ctx, props) {
629
+ try {
630
+ if (props?.session?.id) return String(props.session.id);
631
+ if (props?.session?.sessionId) return String(props.session.sessionId);
632
+ if (props?.input?.sessionId) return String(props.input.sessionId);
633
+ if (ctx?.scope?.session) return String(ctx.scope.session.id || ctx.scope.session.header?.id || '');
634
+ if (typeof ctx?.scope?.id === 'string') return ctx.scope.id;
635
+ if (ctx?.session) return String(ctx.session.id || ctx.session.header?.id || '');
636
+ if (typeof window !== 'undefined' && window.location) {
637
+ const params = new URLSearchParams(window.location.search);
638
+ const fromUrl = params.get('session') || params.get('sessionId');
639
+ if (fromUrl) return fromUrl;
640
+ }
641
+ } catch (_) {}
642
+ return 'default';
643
+ }
644
+
645
+ // --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
646
+ function GoalTopBanner(props) {
647
+ const { ctx } = props || {};
648
+ const sid = sessionIdOf(ctx, props);
649
+ const [state, setState] = useState(null);
650
+ const [isModalOpen, setIsModalOpen] = useState(false);
651
+ const [now, setNow] = useState(() => Date.now());
652
+ const stateRef = useRef(null);
653
+ stateRef.current = state;
654
+
655
+ const t = (key) => {
656
+ if (ctx?.locale?.getSnapshot) {
657
+ const snap = ctx.locale.getSnapshot();
658
+ const active = snap?.active || 'ru';
659
+ return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
660
+ }
661
+ return LOCALES.ru[key] || LOCALES.en[key] || key;
662
+ };
663
+
664
+ useEffect(() => {
665
+ injectStylesOnce();
666
+ }, []);
667
+
668
+ // Локальный секундный интервал для плавного и бесперебойного тикания таймера
669
+ useEffect(() => {
670
+ const ticker = setInterval(() => {
671
+ setNow(Date.now());
672
+ }, 1000);
673
+ return () => clearInterval(ticker);
674
+ }, []);
675
+
676
+ const fetchState = useCallback(async () => {
677
+ try {
678
+ const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
679
+ const res = await fetch(`/dsh-goal/state${query}`, {
680
+ headers: { 'x-dsh-session-id': sid },
681
+ });
682
+ if (res.ok) {
683
+ const data = await res.json();
684
+ setState(data);
685
+ }
686
+ } catch (_) {}
687
+ }, [sid]);
688
+
689
+ // Адаптивный поллинг: 1.5s когда RUNNING и вкладка активна, 8s когда IDLE/PAUSED/COMPLETED или вкладка скрыта
690
+ useEffect(() => {
691
+ let timer = null;
692
+ let isDisposed = false;
693
+
694
+ const scheduleNextPoll = () => {
695
+ if (isDisposed) return;
696
+ const currentState = stateRef.current;
697
+ const isDocHidden = typeof document !== 'undefined' && document.hidden;
698
+ const isRunning = currentState?.hasActiveGoal && currentState?.state === 'RUNNING';
699
+
700
+ let delay = 1500;
701
+ if (isDocHidden) {
702
+ delay = 10000;
703
+ } else if (!isRunning) {
704
+ delay = 8000;
705
+ }
706
+
707
+ timer = setTimeout(async () => {
708
+ await fetchState();
709
+ scheduleNextPoll();
710
+ }, delay);
711
+ };
712
+
713
+ fetchState().then(() => {
714
+ scheduleNextPoll();
715
+ });
716
+
717
+ const onVisibilityChange = () => {
718
+ if (typeof document !== 'undefined' && !document.hidden) {
719
+ if (timer) clearTimeout(timer);
720
+ fetchState().then(() => {
721
+ scheduleNextPoll();
722
+ });
723
+ }
724
+ };
725
+
726
+ if (typeof document !== 'undefined' && document.addEventListener) {
727
+ document.addEventListener('visibilitychange', onVisibilityChange);
728
+ }
729
+
730
+ return () => {
731
+ isDisposed = true;
732
+ if (timer) clearTimeout(timer);
733
+ if (typeof document !== 'undefined' && document.removeEventListener) {
734
+ document.removeEventListener('visibilitychange', onVisibilityChange);
735
+ }
736
+ };
737
+ }, [fetchState]);
738
+
739
+ const handleAction = async (action, extra = {}) => {
740
+ try {
741
+ const res = await fetch('/dsh-goal/action', {
742
+ method: 'POST',
743
+ headers: {
744
+ 'Content-Type': 'application/json',
745
+ 'x-dsh-session-id': sid,
746
+ },
747
+ body: JSON.stringify({ action, sessionId: sid, ...extra }),
748
+ });
749
+ if (res.ok) {
750
+ const data = await res.json();
751
+ if (data.state) setState(data.state);
752
+ }
753
+ } catch (_) {}
754
+ };
755
+
756
+ if (!state || !state.hasActiveGoal) {
757
+ return null;
758
+ }
759
+
760
+ const isPaused = state.state === 'PAUSED';
761
+ const isCompleted = state.state === 'COMPLETED';
762
+
763
+ const formatLiveElapsed = () => {
764
+ if (!state) return '0s';
765
+ if (!state.startedAt) return state.formattedElapsed || '0s';
766
+ const endTime = state.completedAt || (state.pausedAt || now);
767
+ const elapsedSec = Math.max(0, Math.floor((endTime - state.startedAt - (state.totalPausedDurationMs || 0)) / 1000));
768
+ return formatElapsed(elapsedSec);
769
+ };
770
+
771
+ const liveElapsedStr = formatLiveElapsed();
772
+
773
+ return React.createElement(
774
+ React.Fragment,
775
+ null,
776
+ React.createElement(
777
+ 'div',
778
+ { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
779
+ React.createElement(
780
+ 'div',
781
+ { className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
782
+ React.createElement(
783
+ 'div',
784
+ { className: 'dsh-goal-left' },
785
+ React.createElement(
786
+ 'span',
787
+ { className: 'dsh-goal-icon' },
788
+ isCompleted ? React.createElement(IconCheck, { size: 16 }) : React.createElement(IconTarget, { size: 15 }),
789
+ ),
790
+ React.createElement(
791
+ 'span',
792
+ { className: `dsh-goal-label${isCompleted ? ' completed' : ''}` },
793
+ isCompleted ? (t('goalCompleted') || 'Цель выполнена') : (t('goalLabel') || 'Текущая цель'),
794
+ ),
795
+ React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
796
+ ),
797
+ React.createElement(
798
+ 'div',
799
+ { className: 'dsh-goal-right' },
800
+ React.createElement(
801
+ 'span',
802
+ { className: 'dsh-goal-time' },
803
+ `⏱ ${liveElapsedStr}`,
804
+ ),
805
+ !isCompleted
806
+ ? React.createElement(
807
+ 'button',
808
+ {
809
+ className: 'dsh-goal-btn',
810
+ title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
811
+ onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
812
+ },
813
+ isPaused ? React.createElement(IconPlay, { size: 14 }) : React.createElement(IconPause, { size: 14 }),
814
+ )
815
+ : null,
816
+ React.createElement(
817
+ 'button',
818
+ {
819
+ className: 'dsh-goal-btn',
820
+ title: t('details') || 'Развернуть детали цели',
821
+ onClick: () => setIsModalOpen(true),
822
+ },
823
+ React.createElement(IconExpand, { size: 14 }),
824
+ ),
825
+ isCompleted
826
+ ? React.createElement(
827
+ 'button',
828
+ {
829
+ className: 'dsh-goal-btn close',
830
+ title: t('closeBanner') || 'Закрыть плашку цели',
831
+ onClick: () => handleAction('clear'),
832
+ },
833
+ '✕',
834
+ )
835
+ : null,
836
+ ),
837
+ ),
838
+ ),
839
+ isModalOpen
840
+ ? React.createElement(GoalDetailsModal, {
841
+ state,
842
+ onClose: () => setIsModalOpen(false),
843
+ onAction: handleAction,
844
+ t,
845
+ })
846
+ : null,
847
+ );
848
+ }
849
+
850
+ // --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
851
+ // Архитектурное решение (Issue #23):
852
+ // Чистая логика состояния формы настроек вынесена в `lib/card-form-state.js` для запуска
853
+ // быстрых изолированных unit-тестов через `node --test`. В `lib/client.js` эти функции
854
+ // продублированы встроенным образом, поскольку клиентский бандл загружается браузерным
855
+ // загрузчиком DSH (`window.__ModuleLoader__`) как самодостаточный автономный скрипт
856
+ // без этапа сборки (bundler / webpack) и не может динамически импортировать локальные файлы.
857
+ const DEFAULT_SETTINGS = {
858
+ maxIterations: 25,
859
+ autoDrive: true,
860
+ enableSound: true,
861
+ };
862
+
863
+ function parseNumberField(raw) {
864
+ if (raw === '' || raw === null || raw === undefined) {
865
+ return undefined;
866
+ }
867
+ const n = Number(raw);
868
+ if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
869
+ return NaN;
870
+ }
871
+ return n;
872
+ }
873
+
874
+ function getFieldStatus(field, draftValue, snap) {
875
+ const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
876
+ const userVal = snap?.user?.[field];
877
+ const effectiveVal = snap?.value?.[field] ?? baseVal;
878
+
879
+ const isOverridden = userVal !== undefined && userVal !== null;
880
+ const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
881
+
882
+ let invalid = false;
883
+ let isDirty = false;
884
+
885
+ if (field === 'maxIterations') {
886
+ if (draftValue !== undefined) {
887
+ if (draftValue === '') {
888
+ invalid = false;
889
+ isDirty = userVal !== undefined;
890
+ } else {
891
+ const parsed = parseNumberField(draftValue);
892
+ if (Number.isNaN(parsed)) {
893
+ invalid = true;
894
+ isDirty = true;
895
+ } else {
896
+ isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
897
+ }
898
+ }
899
+ }
900
+ } else {
901
+ if (draftValue !== undefined) {
902
+ isDirty = draftValue !== (userVal !== undefined ? userVal : baseVal);
903
+ }
904
+ }
905
+
906
+ return {
907
+ value: currentVal,
908
+ baseValue: baseVal,
909
+ userValue: userVal,
910
+ isOverridden,
911
+ isDirty,
912
+ invalid,
913
+ };
914
+ }
915
+
916
+ function computeSavePlan(draft, snap) {
917
+ if (!draft) return [];
918
+
919
+ const writes = [];
920
+ const fields = ['maxIterations', 'autoDrive', 'enableSound'];
921
+
922
+ for (const f of fields) {
923
+ if (draft[f] === undefined) continue;
924
+
925
+ const status = getFieldStatus(f, draft[f], snap);
926
+ if (status.invalid) continue;
927
+
928
+ if (status.isDirty) {
929
+ if (f === 'maxIterations' && draft[f] === '') {
930
+ writes.push([f, undefined]);
931
+ } else if (f === 'maxIterations') {
932
+ writes.push([f, parseNumberField(draft[f])]);
933
+ } else {
934
+ writes.push([f, Boolean(draft[f])]);
935
+ }
936
+ }
937
+ }
938
+
939
+ return writes;
940
+ }
941
+
942
+ // --- КАРТОЧКА НАСТРОЕК (SETTINGS CARD) ---
943
+ function GoalSettingsCard(props) {
944
+ const { scope, ctx } = props || {};
945
+ const [isOpen, setIsOpen] = useState(false);
946
+ const [draft, setDraft] = useState(null);
947
+ const [saving, setSaving] = useState(false);
948
+ const [failed, setFailed] = useState(false);
949
+
950
+ const [snap, setSnap] = useState(() => (scope ? scope.getSnapshot() : { value: DEFAULT_SETTINGS, base: DEFAULT_SETTINGS, user: {} }));
951
+
952
+ useEffect(() => {
953
+ injectStylesOnce();
954
+ }, []);
955
+
956
+ useEffect(() => {
957
+ if (!scope || typeof scope.subscribe !== 'function') return;
958
+ return scope.subscribe((next) => {
959
+ setSnap(next || scope.getSnapshot());
960
+ });
961
+ }, [scope]);
962
+
963
+ const t = (key) => {
964
+ if (ctx?.locale?.getSnapshot) {
965
+ const snapLoc = ctx.locale.getSnapshot();
966
+ const active = snapLoc?.active || 'ru';
967
+ return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
968
+ }
969
+ return LOCALES.ru[key] || LOCALES.en[key] || key;
970
+ };
971
+
972
+ const status = (snap && snap.status) || 'ready';
973
+ const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
974
+ const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
975
+ const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
976
+
977
+ const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
978
+ const invalid = maxIterStatus.invalid;
979
+ const disabled = !scope || saving || snap?.writable === false;
980
+
981
+ const edit = (field, val) => {
982
+ setFailed(false);
983
+ setDraft((prev) => ({
984
+ maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
985
+ autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
986
+ enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
987
+ [field]: val,
988
+ }));
989
+ };
990
+
991
+ const resetFieldToDefault = (field) => {
992
+ if (field === 'maxIterations') {
993
+ edit(field, '');
994
+ } else {
995
+ edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
996
+ }
997
+ };
998
+
999
+ const save = async () => {
1000
+ if (!scope || !dirty || invalid) return;
1001
+ setSaving(true);
1002
+ setFailed(false);
1003
+
1004
+ const writes = computeSavePlan(draft, snap);
1005
+ let landed = true;
1006
+
1007
+ for (const [field, fieldValue] of writes) {
1008
+ try {
1009
+ await scope.set(field, fieldValue);
1010
+ } catch (err) {
1011
+ console.error('[dsh-goal] failed to save setting', field, err);
1012
+ landed = false;
1013
+ }
1014
+ }
1015
+
1016
+ const fresh = scope.getSnapshot();
1017
+ const user = fresh.user || {};
1018
+ for (const [field, fieldValue] of writes) {
1019
+ if (fieldValue === undefined) {
1020
+ if (user[field] !== undefined) landed = false;
1021
+ } else {
1022
+ if (user[field] !== fieldValue) landed = false;
1023
+ }
1024
+ }
1025
+
1026
+ setSnap(fresh);
1027
+ setSaving(false);
1028
+ if (landed) {
1029
+ setDraft(null);
1030
+ } else {
1031
+ setFailed(true);
1032
+ }
1033
+ };
1034
+
1035
+ const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
1036
+
1037
+ return React.createElement(
1038
+ 'li',
1039
+ { className: 'dsh-goal-card' },
1040
+ React.createElement(
1041
+ 'button',
1042
+ {
1043
+ className: 'dsh-goal-card-head',
1044
+ onClick: () => setIsOpen(!isOpen),
1045
+ 'aria-expanded': isOpen,
1046
+ },
1047
+ React.createElement(
1048
+ 'div',
1049
+ { style: { flex: 1 } },
1050
+ React.createElement('div', { className: 'dsh-goal-card-title' }, t('pluginTitle') || 'Цели и автономный режим (Goal Mode)'),
1051
+ React.createElement('div', { className: 'dsh-goal-card-sub' }, t('pluginDesc') || 'Верхняя закреплённая панель цели, декомпозиция вех и авто-драйв'),
1052
+ ),
1053
+ React.createElement(Chevron, { className: `dsh-goal-chev ${isOpen ? 'dsh-goal-chev-open' : ''}` }),
1054
+ ),
1055
+ isOpen
1056
+ ? React.createElement(
1057
+ 'div',
1058
+ { className: 'dsh-goal-card-body' },
1059
+ status === 'loading'
1060
+ ? React.createElement('p', { className: 'dsh-goal-field-hint', style: { padding: '12px 0' } }, t('loading') || 'Загрузка настроек…')
1061
+ : status !== 'ready'
1062
+ ? React.createElement('p', { className: 'dsh-goal-foot-note', style: { padding: '12px 0' } }, t('unavailable') || 'Настройки недоступны')
1063
+ : React.createElement(
1064
+ React.Fragment,
1065
+ null,
1066
+ React.createElement(
1067
+ 'div',
1068
+ { className: 'dsh-goal-card-field' },
1069
+ React.createElement(
1070
+ 'div',
1071
+ { className: 'dsh-goal-field-label-row' },
1072
+ React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1073
+ React.createElement(
1074
+ 'div',
1075
+ { className: 'dsh-goal-field-meta' },
1076
+ maxIterStatus.isOverridden
1077
+ ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1078
+ : null,
1079
+ maxIterStatus.isOverridden
1080
+ ? React.createElement(
1081
+ 'button',
1082
+ {
1083
+ type: 'button',
1084
+ className: 'dsh-goal-btn-inline-reset',
1085
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1086
+ onClick: () => resetFieldToDefault('maxIterations'),
1087
+ },
1088
+ React.createElement(IconRotateCcw, { size: 12 }),
1089
+ t('resetField') || 'По умолчанию',
1090
+ )
1091
+ : null,
1092
+ ),
1093
+ ),
1094
+ React.createElement('input', {
1095
+ id: 'dsh-goal-max-iter',
1096
+ type: 'number',
1097
+ min: 1,
1098
+ placeholder: String(maxIterStatus.baseValue),
1099
+ className: 'dsh-goal-card-input',
1100
+ value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1101
+ disabled,
1102
+ 'aria-invalid': invalid,
1103
+ onChange: (e) => edit('maxIterations', e.target.value),
1104
+ }),
1105
+ ),
1106
+ React.createElement(
1107
+ 'div',
1108
+ { className: 'dsh-goal-check-row' },
1109
+ React.createElement(
1110
+ 'label',
1111
+ { className: 'dsh-goal-check' },
1112
+ React.createElement('input', {
1113
+ type: 'checkbox',
1114
+ checked: autoDriveStatus.value === true,
1115
+ disabled,
1116
+ onChange: (e) => edit('autoDrive', e.target.checked),
1117
+ }),
1118
+ t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1119
+ ),
1120
+ autoDriveStatus.isOverridden
1121
+ ? React.createElement(
1122
+ 'button',
1123
+ {
1124
+ type: 'button',
1125
+ className: 'dsh-goal-btn-inline-reset',
1126
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1127
+ onClick: () => resetFieldToDefault('autoDrive'),
1128
+ },
1129
+ React.createElement(IconRotateCcw, { size: 12 }),
1130
+ )
1131
+ : null,
1132
+ ),
1133
+ React.createElement(
1134
+ 'div',
1135
+ { className: 'dsh-goal-check-row' },
1136
+ React.createElement(
1137
+ 'label',
1138
+ { className: 'dsh-goal-check' },
1139
+ React.createElement('input', {
1140
+ type: 'checkbox',
1141
+ checked: enableSoundStatus.value === true,
1142
+ disabled,
1143
+ onChange: (e) => edit('enableSound', e.target.checked),
1144
+ }),
1145
+ t('soundLabel') || 'Звук по завершении цели',
1146
+ ),
1147
+ enableSoundStatus.isOverridden
1148
+ ? React.createElement(
1149
+ 'button',
1150
+ {
1151
+ type: 'button',
1152
+ className: 'dsh-goal-btn-inline-reset',
1153
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1154
+ onClick: () => resetFieldToDefault('enableSound'),
1155
+ },
1156
+ React.createElement(IconRotateCcw, { size: 12 }),
1157
+ )
1158
+ : null,
1159
+ ),
1160
+ React.createElement(
1161
+ 'div',
1162
+ { className: 'dsh-goal-foot' },
1163
+ failed
1164
+ ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1165
+ : null,
1166
+ React.createElement(
1167
+ 'button',
1168
+ {
1169
+ className: 'dsh-goal-discard',
1170
+ disabled: !dirty || disabled,
1171
+ onClick: () => {
1172
+ setFailed(false);
1173
+ setDraft(null);
1174
+ },
1175
+ },
1176
+ t('discard') || 'Отменить правки',
1177
+ ),
1178
+ React.createElement(
1179
+ 'button',
1180
+ { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1181
+ saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1182
+ ),
1183
+ ),
1184
+ )
1185
+ )
1186
+ : null,
1187
+ );
1188
+ }
1189
+
1190
+ // --- СЛОВАРИ ЛОКАЛИЗАЦИИ ---
1191
+ const LOCALES = {
1192
+ ru: {
1193
+ goalLabel: 'Текущая цель',
1194
+ goalCompleted: 'Цель выполнена',
1195
+ clearGoal: 'Очистить цель',
1196
+ closeBanner: 'Закрыть плашку цели',
1197
+ pause: 'Приостановить',
1198
+ resume: 'Возобновить',
1199
+ details: 'Развернуть детали цели',
1200
+ modalTitle: 'План и статус цели',
1201
+ modalTitleCompleted: 'Цель выполнена: план и результаты',
1202
+ time: 'Время',
1203
+ milestones: 'План работ:',
1204
+ noMilestones: 'Агент формирует план работ...',
1205
+ close: 'Закрыть',
1206
+ pluginTitle: 'Цели и автономный режим (Goal Mode)',
1207
+ pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
1208
+ maxIterLabel: 'Максимум итераций (Safety Limit):',
1209
+ autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
1210
+ soundLabel: 'Звук по завершении цели',
1211
+ save: 'Сохранить',
1212
+ saving: 'Сохранение…',
1213
+ discard: 'Отменить правки',
1214
+ resetField: 'По умолчанию',
1215
+ overridden: 'изменено',
1216
+ saveFailed: 'Не сохранено — исправьте и повторите',
1217
+ loading: 'Загрузка настроек…',
1218
+ unavailable: 'Настройки недоступны',
1219
+ },
1220
+ en: {
1221
+ goalLabel: 'Current goal',
1222
+ goalCompleted: 'Goal completed',
1223
+ clearGoal: 'Clear goal',
1224
+ closeBanner: 'Close goal banner',
1225
+ pause: 'Pause',
1226
+ resume: 'Resume',
1227
+ details: 'Expand Goal Details',
1228
+ modalTitle: 'Goal Plan & Status',
1229
+ modalTitleCompleted: 'Goal Completed: Plan & Results',
1230
+ time: 'Time',
1231
+ milestones: 'Plan of work:',
1232
+ noMilestones: 'Agent is preparing the plan of work...',
1233
+ close: 'Close',
1234
+ pluginTitle: 'Goal Mode & Autonomous Loop',
1235
+ pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
1236
+ maxIterLabel: 'Max iterations (Safety Limit):',
1237
+ autoDriveLabel: 'Auto-drive: keep the loop running automatically',
1238
+ soundLabel: 'Sound when a goal completes',
1239
+ save: 'Save',
1240
+ saving: 'Saving…',
1241
+ discard: 'Discard changes',
1242
+ resetField: 'Default',
1243
+ overridden: 'overridden',
1244
+ saveFailed: 'Not saved — fix the values and try again',
1245
+ loading: 'Loading settings…',
1246
+ unavailable: 'Settings unavailable',
1247
+ },
1248
+ };
1249
+
1250
+ module.exports.inject = ['slots', 'locale', 'settingsScope'];
1251
+ module.exports.apply = function apply(ctx) {
1252
+ // Регистрация локализации
1253
+ try {
1254
+ ctx.locale?.register?.(NS, LOCALES);
1255
+ } catch (_) {}
1256
+
1257
+ // Пространство настроек плагина
1258
+ const settingsScope =
1259
+ ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
1260
+ ? ctx.settingsScope.bind({ namespace: NS })
1261
+ : null;
1262
+
1263
+ const registerSlotSafe = (name, entry, comp) => {
1264
+ try {
1265
+ if (typeof ctx.slots?.inject === 'function') {
1266
+ ctx.slots.inject(name, () => {
1267
+ try {
1268
+ return ctx.slots.register(entry, comp);
1269
+ } catch (_) {}
1270
+ });
1271
+ } else if (typeof ctx.slots?.register === 'function') {
1272
+ ctx.slots.register(entry, comp);
1273
+ }
1274
+ } catch (_) {}
1275
+ };
1276
+
1277
+ // 1. Регистрация виджета цели над полем ввода (composer dock)
1278
+ registerSlotSafe(
1279
+ 'conversation.input.dock',
1280
+ {
1281
+ name: 'conversation.input.dock',
1282
+ id: '@goodandready/dsh-goal',
1283
+ order: 10,
1284
+ locale: NS,
1285
+ inject: (slotProps) => ({ ctx, ...slotProps }),
1286
+ },
1287
+ GoalTopBanner,
1288
+ );
1289
+
1290
+ // 2. Регистрация карточки настроек
1291
+ registerSlotSafe(
1292
+ 'settings.plugin.item',
1293
+ {
1294
+ name: 'settings.plugin.item',
1295
+ key: NS,
1296
+ locale: NS,
1297
+ inject: () => ({ ctx, scope: settingsScope }),
1298
+ },
1299
+ GoalSettingsCard,
1300
+ );
1301
+ };
1302
+
1303
+ return module.exports;
1304
+ },
1305
+ });