@goodandready/dsh-goal 0.1.4 → 0.1.5

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,1305 +1,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 } = 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
- });
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
+ });