@goodandready/dsh-goal 0.1.1 → 0.1.3

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
@@ -17,7 +17,7 @@ window.__ModuleLoader__.load({
17
17
 
18
18
  // --- SVG ИКОНКИ ---
19
19
 
20
- function IconTarget({ size = 16, className = '' }) {
20
+ function IconTarget({ size = 15, className = '' }) {
21
21
  return React.createElement(
22
22
  'svg',
23
23
  {
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
37
37
  );
38
38
  }
39
39
 
40
- function IconTrash({ size = 15, className = '' }) {
40
+ function IconTrash({ size = 14, className = '' }) {
41
41
  return React.createElement(
42
42
  'svg',
43
43
  {
@@ -57,7 +57,7 @@ window.__ModuleLoader__.load({
57
57
  );
58
58
  }
59
59
 
60
- function IconPause({ size = 15, className = '' }) {
60
+ function IconPause({ size = 14, className = '' }) {
61
61
  return React.createElement(
62
62
  'svg',
63
63
  {
@@ -77,7 +77,7 @@ window.__ModuleLoader__.load({
77
77
  );
78
78
  }
79
79
 
80
- function IconPlay({ size = 15, className = '' }) {
80
+ function IconPlay({ size = 14, className = '' }) {
81
81
  return React.createElement(
82
82
  'svg',
83
83
  {
@@ -96,7 +96,26 @@ window.__ModuleLoader__.load({
96
96
  );
97
97
  }
98
98
 
99
- function IconExpand({ size = 15, className = '' }) {
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 = '' }) {
100
119
  return React.createElement(
101
120
  'svg',
102
121
  {
@@ -117,6 +136,25 @@ window.__ModuleLoader__.load({
117
136
  );
118
137
  }
119
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
+
120
158
  function IconChevronDown({ size = 14, className = '' }) {
121
159
  return React.createElement(
122
160
  'svg',
@@ -135,22 +173,31 @@ window.__ModuleLoader__.load({
135
173
  );
136
174
  }
137
175
 
138
- // --- СТИЛИ И CSS ПРАВИЛА ---
176
+ // --- СТИЛИ И CSS ПРАВИЛА (ЧИСТЫЕ DSH ТОКЕНЫ БЕЗ ХАРДКОД-ФОЛЛБЕКОВ) ---
139
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
+ }
140
186
  .dsh-goal-banner {
187
+ box-sizing: border-box;
188
+ width: 100%;
141
189
  display: flex;
142
190
  align-items: center;
143
191
  justify-content: space-between;
144
- background: var(--dsw-alias-bg-layer-3, #1e1e20);
145
- border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.08));
146
- border-radius: 10px;
147
- padding: 8px 14px;
148
- margin: 6px 12px;
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;
149
196
  font-family: inherit;
150
197
  font-size: 13px;
151
- color: var(--dsw-alias-label-primary, #ffffff);
198
+ color: var(--dsw-alias-label-primary);
152
199
  user-select: none;
153
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
200
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
154
201
  transition: all 0.2s ease;
155
202
  }
156
203
  .dsh-goal-left {
@@ -161,33 +208,43 @@ window.__ModuleLoader__.load({
161
208
  flex: 1;
162
209
  }
163
210
  .dsh-goal-icon {
164
- color: var(--dsw-alias-label-secondary, #9da2a8);
211
+ color: var(--dsw-alias-label-secondary);
165
212
  flex-shrink: 0;
166
213
  display: flex;
167
214
  align-items: center;
168
215
  }
169
216
  .dsh-goal-label {
170
217
  font-weight: 600;
171
- color: var(--dsw-alias-label-primary, #ffffff);
218
+ color: var(--dsw-alias-label-primary);
172
219
  white-space: nowrap;
173
220
  }
221
+ .dsh-goal-label.completed {
222
+ color: var(--dsw-alias-status-success);
223
+ }
174
224
  .dsh-goal-title {
175
- color: var(--dsw-alias-label-secondary, #b4b8be);
225
+ color: var(--dsw-alias-label-secondary);
176
226
  overflow: hidden;
177
227
  text-overflow: ellipsis;
178
228
  white-space: nowrap;
179
229
  font-weight: 400;
230
+ flex: 1;
231
+ min-width: 0;
180
232
  }
181
233
  .dsh-goal-time {
182
- color: var(--dsw-alias-label-tertiary, #6e737a);
234
+ color: var(--dsw-alias-label-secondary);
183
235
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
184
236
  font-size: 12px;
237
+ font-weight: 500;
185
238
  white-space: nowrap;
239
+ display: inline-flex;
240
+ align-items: center;
241
+ gap: 4px;
242
+ flex-shrink: 0;
186
243
  }
187
244
  .dsh-goal-right {
188
245
  display: flex;
189
246
  align-items: center;
190
- gap: 10px;
247
+ gap: 8px;
191
248
  margin-left: 12px;
192
249
  flex-shrink: 0;
193
250
  }
@@ -196,7 +253,7 @@ window.__ModuleLoader__.load({
196
253
  border: 0;
197
254
  padding: 4px;
198
255
  margin: 0;
199
- color: var(--dsw-alias-label-tertiary, #888d94);
256
+ color: var(--dsw-alias-label-tertiary);
200
257
  cursor: pointer;
201
258
  display: flex;
202
259
  align-items: center;
@@ -205,11 +262,19 @@ window.__ModuleLoader__.load({
205
262
  transition: all 0.15s ease;
206
263
  }
207
264
  .dsh-goal-btn:hover {
208
- color: var(--dsw-alias-label-primary, #ffffff);
209
- background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06));
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);
210
275
  }
211
276
  .dsh-goal-btn.danger:hover {
212
- color: var(--dsw-alias-status-danger, #ff5c5c);
277
+ color: var(--dsw-alias-status-danger);
213
278
  }
214
279
 
215
280
  /* Modal Details */
@@ -224,15 +289,15 @@ window.__ModuleLoader__.load({
224
289
  backdrop-filter: blur(4px);
225
290
  }
226
291
  .dsh-goal-modal {
227
- background: var(--dsw-alias-bg-layer-3, #1e1e20);
228
- border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.1));
292
+ background: var(--dsw-alias-bg-layer-3);
293
+ border: 1px solid var(--dsw-alias-border-l2);
229
294
  border-radius: 12px;
230
295
  width: 520px;
231
296
  max-width: 90vw;
232
297
  max-height: 80vh;
233
298
  display: flex;
234
299
  flex-direction: column;
235
- box-shadow: 0 16px 36px rgba(0,0,0,0.4);
300
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
236
301
  }
237
302
  .dsh-goal-modal-head {
238
303
  padding: 14px 18px;
@@ -253,17 +318,6 @@ window.__ModuleLoader__.load({
253
318
  flex-direction: column;
254
319
  gap: 12px;
255
320
  }
256
- .dsh-goal-progress-bar {
257
- height: 6px;
258
- background: var(--dsw-alias-bg-layer-2, #2c2d30);
259
- border-radius: 3px;
260
- overflow: hidden;
261
- }
262
- .dsh-goal-progress-fill {
263
- height: 100%;
264
- background: var(--dsw-alias-status-success, #22c55e);
265
- transition: width 0.3s ease;
266
- }
267
321
  .dsh-goal-milestone-item {
268
322
  display: flex;
269
323
  align-items: flex-start;
@@ -325,6 +379,27 @@ window.__ModuleLoader__.load({
325
379
  gap: 6px;
326
380
  padding: 12px 0;
327
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
+ }
328
403
  .dsh-goal-card-input {
329
404
  height: 34px;
330
405
  border: 1px solid var(--dsw-alias-border-l2);
@@ -334,6 +409,9 @@ window.__ModuleLoader__.load({
334
409
  padding: 0 12px;
335
410
  font-size: 13px;
336
411
  }
412
+ .dsh-goal-card-input[aria-invalid="true"] {
413
+ border-color: var(--dsw-alias-status-danger);
414
+ }
337
415
  .dsh-goal-chev {
338
416
  margin-left: auto;
339
417
  flex: none;
@@ -343,9 +421,90 @@ window.__ModuleLoader__.load({
343
421
  .dsh-goal-chev-open {
344
422
  transform: rotate(180deg);
345
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
+ }
346
504
  `;
347
505
 
348
506
  function injectStylesOnce() {
507
+ if (typeof document === 'undefined') return;
349
508
  if (document.getElementById('dsh-goal-styles')) return;
350
509
  const style = document.createElement('style');
351
510
  style.id = 'dsh-goal-styles';
@@ -358,8 +517,7 @@ window.__ModuleLoader__.load({
358
517
  if (!state) return null;
359
518
 
360
519
  const milestones = state.milestones || [];
361
- const completedCount = milestones.filter((m) => m.status === 'completed').length;
362
- const totalCount = milestones.length;
520
+ const isCompleted = state.state === 'COMPLETED';
363
521
 
364
522
  return React.createElement(
365
523
  'div',
@@ -378,12 +536,16 @@ window.__ModuleLoader__.load({
378
536
  React.createElement(
379
537
  'div',
380
538
  { style: { display: 'flex', alignItems: 'center', gap: 8 } },
381
- React.createElement(IconTarget, { size: 18 }),
382
- React.createElement('span', { className: 'dsh-goal-modal-title' }, t('modalTitle') || 'Цель и вехи выполнения'),
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
+ ),
383
545
  ),
384
546
  React.createElement(
385
547
  'button',
386
- { className: 'dsh-goal-btn', onClick: onClose },
548
+ { className: 'dsh-goal-btn', onClick: onClose, title: t('close') || 'Закрыть' },
387
549
  '✕',
388
550
  ),
389
551
  ),
@@ -393,36 +555,40 @@ window.__ModuleLoader__.load({
393
555
  React.createElement(
394
556
  'div',
395
557
  null,
396
- React.createElement('div', { style: { fontWeight: 600, marginBottom: 4 } }, state.title),
397
- state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, state.description) : 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,
398
560
  ),
399
561
  React.createElement(
400
562
  'div',
401
- { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
402
- React.createElement('span', null, `${t('progress') || 'Прогресс'}: ${completedCount}/${totalCount} (${state.progressPercent || 0}%)`),
563
+ { style: { display: 'flex', justifyContent: 'flex-end', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
403
564
  React.createElement('span', null, `${t('time') || 'Время'}: ${state.formattedElapsed || '0s'}`),
404
565
  ),
405
566
  React.createElement(
406
567
  'div',
407
- { className: 'dsh-goal-progress-bar' },
408
- React.createElement('div', { className: 'dsh-goal-progress-fill', style: { width: `${state.progressPercent || 0}%` } }),
409
- ),
410
- React.createElement(
411
- 'div',
412
- { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 } },
413
- React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'Шаги выполнения:'),
568
+ { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
569
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'План работ:'),
414
570
  milestones.length === 0
415
- ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 } }, t('noMilestones') || 'Агент декомпозирует шаги автоматически...')
571
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
416
572
  : milestones.map((m, i) =>
417
573
  React.createElement(
418
574
  'div',
419
575
  { key: m.id || i, className: 'dsh-goal-milestone-item' },
420
- React.createElement('span', null, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
576
+ React.createElement('span', { style: { fontSize: 14 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
421
577
  React.createElement(
422
578
  'div',
423
- { style: { flex: 1 } },
424
- React.createElement('div', { style: { textDecoration: m.status === 'completed' ? 'line-through' : 'none' } }, m.title),
425
- m.notes ? React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, m.notes) : null,
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,
426
592
  ),
427
593
  ),
428
594
  ),
@@ -431,12 +597,26 @@ window.__ModuleLoader__.load({
431
597
  React.createElement(
432
598
  'div',
433
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,
434
614
  React.createElement(
435
615
  'button',
436
616
  {
437
- className: 'dsh-goal-btn',
438
- style: { padding: '6px 14px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
617
+ className: 'dsh-goal-save',
439
618
  onClick: onClose,
619
+ style: { padding: '6px 16px' },
440
620
  },
441
621
  t('close') || 'Закрыть',
442
622
  ),
@@ -445,11 +625,33 @@ window.__ModuleLoader__.load({
445
625
  );
446
626
  }
447
627
 
448
- // --- ГЛАВНЫЙ КОМПОНЕНТ: TOP BANNER (В ТОЧНОСТИ КАК НА СКРИНШОТЕ) ---
628
+ // --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
449
629
  function GoalTopBanner(props) {
630
+ const { ctx } = props || {};
450
631
  const [state, setState] = useState(null);
451
632
  const [isModalOpen, setIsModalOpen] = useState(false);
452
- const t = props.t || ((k) => k);
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
+ }, []);
453
655
 
454
656
  const fetchState = useCallback(async () => {
455
657
  try {
@@ -462,89 +664,105 @@ window.__ModuleLoader__.load({
462
664
  }, []);
463
665
 
464
666
  useEffect(() => {
465
- injectStylesOnce();
466
667
  fetchState();
467
- const interval = setInterval(fetchState, 1000);
468
- return () => clearInterval(interval);
668
+ const timer = setInterval(fetchState, 1500);
669
+ return () => clearInterval(timer);
469
670
  }, [fetchState]);
470
671
 
471
672
  const handleAction = async (action, extra = {}) => {
472
673
  try {
473
- await fetch('/dsh-goal/action', {
674
+ const res = await fetch('/dsh-goal/action', {
474
675
  method: 'POST',
475
676
  headers: { 'Content-Type': 'application/json' },
476
677
  body: JSON.stringify({ action, ...extra }),
477
678
  });
478
- fetchState();
679
+ if (res.ok) {
680
+ const data = await res.json();
681
+ if (data.state) setState(data.state);
682
+ }
479
683
  } catch (_) {}
480
684
  };
481
685
 
482
- // Все хуки объявлены выше!
483
686
  if (!state || !state.hasActiveGoal) {
484
- return null; // Если нет активной цели, плашка скрыта
687
+ return null;
485
688
  }
486
689
 
487
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();
488
702
 
489
703
  return React.createElement(
490
704
  React.Fragment,
491
705
  null,
492
706
  React.createElement(
493
707
  'div',
494
- { className: 'dsh-goal-banner' },
495
- React.createElement(
496
- 'div',
497
- { className: 'dsh-goal-left' },
498
- React.createElement(
499
- 'span',
500
- { className: 'dsh-goal-icon' },
501
- React.createElement(IconTarget, { size: 16 }),
502
- ),
503
- React.createElement(
504
- 'span',
505
- { className: 'dsh-goal-label' },
506
- t('goalLabel') || 'Текущая цель',
507
- ),
508
- React.createElement(
509
- 'span',
510
- { className: 'dsh-goal-title', title: state.title },
511
- state.title,
512
- ),
513
- React.createElement(
514
- 'span',
515
- { className: 'dsh-goal-time' },
516
- `• ${state.formattedElapsed || '0s'}`,
517
- ),
518
- ),
708
+ { className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
519
709
  React.createElement(
520
710
  'div',
521
- { className: 'dsh-goal-right' },
711
+ { className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
522
712
  React.createElement(
523
- 'button',
524
- {
525
- className: 'dsh-goal-btn danger',
526
- title: t('clearGoal') || 'Очистить / Отменить цель',
527
- onClick: () => handleAction('clear'),
528
- },
529
- React.createElement(IconTrash, { size: 15 }),
530
- ),
531
- React.createElement(
532
- 'button',
533
- {
534
- className: 'dsh-goal-btn',
535
- title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
536
- onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
537
- },
538
- isPaused ? React.createElement(IconPlay, { size: 15 }) : React.createElement(IconPause, { size: 15 }),
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),
539
726
  ),
540
727
  React.createElement(
541
- 'button',
542
- {
543
- className: 'dsh-goal-btn',
544
- title: t('details') || 'Развернуть детали цели',
545
- onClick: () => setIsModalOpen(true),
546
- },
547
- React.createElement(IconExpand, { size: 15 }),
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,
548
766
  ),
549
767
  ),
550
768
  ),
@@ -559,17 +777,191 @@ window.__ModuleLoader__.load({
559
777
  );
560
778
  }
561
779
 
562
- // --- КАРТОЧКА НАСТРОЕК В SETTINGS.PLUGIN.ITEM ---
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) ---
563
873
  function GoalSettingsCard(props) {
874
+ const { scope, ctx } = props || {};
564
875
  const [isOpen, setIsOpen] = useState(false);
565
- const [maxIter, setMaxIter] = useState(25);
566
- const ctx = props.ctx;
567
- const t = props.t || ((k) => k);
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: {} }));
568
881
 
569
882
  useEffect(() => {
570
883
  injectStylesOnce();
571
884
  }, []);
572
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
+
573
965
  const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
574
966
 
575
967
  return React.createElement(
@@ -594,19 +986,134 @@ window.__ModuleLoader__.load({
594
986
  ? React.createElement(
595
987
  'div',
596
988
  { className: 'dsh-goal-card-body' },
597
- React.createElement(
598
- 'div',
599
- { className: 'dsh-goal-card-field' },
600
- React.createElement('label', { style: { fontSize: 13, fontWeight: 500 } }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
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
+ ),
601
1024
  React.createElement('input', {
1025
+ id: 'dsh-goal-max-iter',
602
1026
  type: 'number',
1027
+ min: 1,
1028
+ placeholder: String(maxIterStatus.baseValue),
603
1029
  className: 'dsh-goal-card-input',
604
- value: maxIter,
605
- onChange: (e) => setMaxIter(Number(e.target.value)),
1030
+ value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1031
+ disabled,
1032
+ 'aria-invalid': invalid,
1033
+ onChange: (e) => edit('maxIterations', e.target.value),
606
1034
  }),
607
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
+ ),
608
1114
  )
609
- : null,
1115
+ )
1116
+ : null,
610
1117
  );
611
1118
  }
612
1119
 
@@ -614,49 +1121,94 @@ window.__ModuleLoader__.load({
614
1121
  const LOCALES = {
615
1122
  ru: {
616
1123
  goalLabel: 'Текущая цель',
617
- clearGoal: 'Очистить / Отменить цель',
1124
+ goalCompleted: 'Цель выполнена',
1125
+ clearGoal: 'Очистить цель',
1126
+ closeBanner: 'Закрыть плашку цели',
618
1127
  pause: 'Приостановить',
619
1128
  resume: 'Возобновить',
620
1129
  details: 'Развернуть детали цели',
621
- modalTitle: 'Детали активной цели',
622
- progress: 'Прогресс',
1130
+ modalTitle: 'План и статус цели',
1131
+ modalTitleCompleted: 'Цель выполнена: план и результаты',
623
1132
  time: 'Время',
624
- milestones: 'Шаги выполнения:',
625
- noMilestones: 'Агент декомпозирует шаги автоматически...',
1133
+ milestones: 'План работ:',
1134
+ noMilestones: 'Агент формирует план работ...',
626
1135
  close: 'Закрыть',
627
1136
  pluginTitle: 'Цели и автономный режим (Goal Mode)',
628
- pluginDesc: 'Верхняя закреплённая панель цели, декомпозиция вех и авто-драйв',
1137
+ pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
629
1138
  maxIterLabel: 'Максимум итераций (Safety Limit):',
1139
+ autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
1140
+ soundLabel: 'Звук по завершении цели',
1141
+ save: 'Сохранить',
1142
+ saving: 'Сохранение…',
1143
+ discard: 'Отменить правки',
1144
+ resetField: 'По умолчанию',
1145
+ overridden: 'изменено',
1146
+ saveFailed: 'Не сохранено — исправьте и повторите',
1147
+ loading: 'Загрузка настроек…',
1148
+ unavailable: 'Настройки недоступны',
630
1149
  },
631
1150
  en: {
632
- goalLabel: 'Current Goal',
633
- clearGoal: 'Clear / Cancel Goal',
1151
+ goalLabel: 'Current goal',
1152
+ goalCompleted: 'Goal completed',
1153
+ clearGoal: 'Clear goal',
1154
+ closeBanner: 'Close goal banner',
634
1155
  pause: 'Pause',
635
1156
  resume: 'Resume',
636
1157
  details: 'Expand Goal Details',
637
- modalTitle: 'Active Goal Details',
638
- progress: 'Progress',
1158
+ modalTitle: 'Goal Plan & Status',
1159
+ modalTitleCompleted: 'Goal Completed: Plan & Results',
639
1160
  time: 'Time',
640
- milestones: 'Milestones:',
641
- noMilestones: 'Agent will decompose steps automatically...',
1161
+ milestones: 'Plan of work:',
1162
+ noMilestones: 'Agent is preparing the plan of work...',
642
1163
  close: 'Close',
643
1164
  pluginTitle: 'Goal Mode & Autonomous Loop',
644
- pluginDesc: 'Sticky top goal banner, milestone decomposition, and auto-drive',
1165
+ pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
645
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',
646
1177
  },
647
1178
  };
648
1179
 
649
- module.exports.inject = ['slots', 'locale'];
1180
+ module.exports.inject = ['slots', 'locale', 'settingsScope'];
650
1181
  module.exports.apply = function apply(ctx) {
651
1182
  // Регистрация локализации
652
1183
  try {
653
1184
  ctx.locale?.register?.(NS, LOCALES);
654
1185
  } catch (_) {}
655
1186
 
656
- // 1. Регистрация Sticky Top Banner в слот рабочей области / сессии
657
- ctx.slots?.register?.(
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',
658
1210
  {
659
- name: 'session.header',
1211
+ name: 'conversation.input.dock',
660
1212
  id: '@goodandready/dsh-goal',
661
1213
  order: 10,
662
1214
  locale: NS,
@@ -665,25 +1217,14 @@ window.__ModuleLoader__.load({
665
1217
  GoalTopBanner,
666
1218
  );
667
1219
 
668
- // Фоллбек: также регистрируем в chat.banner
669
- ctx.slots?.register?.(
670
- {
671
- name: 'chat.banner',
672
- id: '@goodandready/dsh-goal:banner',
673
- order: 5,
674
- locale: NS,
675
- inject: () => ({ ctx }),
676
- },
677
- GoalTopBanner,
678
- );
679
-
680
1220
  // 2. Регистрация карточки настроек
681
- ctx.slots?.register?.(
1221
+ registerSlotSafe(
1222
+ 'settings.plugin.item',
682
1223
  {
683
1224
  name: 'settings.plugin.item',
684
1225
  key: NS,
685
1226
  locale: NS,
686
- inject: () => ({ ctx }),
1227
+ inject: () => ({ ctx, scope: settingsScope }),
687
1228
  },
688
1229
  GoalSettingsCard,
689
1230
  );