@goodandready/dsh-goal 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/design/DESIGN.md +20 -0
- package/lib/client.js +309 -6
- package/lib/command-handler.js +7 -1
- package/lib/goal-engine.js +90 -1
- package/lib/index.js +14 -0
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -96,3 +96,23 @@
|
|
|
96
96
|
- **Устойчивость SSE с Exponential Backoff**: На клиенте внедрена автоматическая схема повторного подключения `EventSource` с возрастающей задержкой (от 2s до 30s) и рандомизированным джиттером для защиты от шторма переподключений.
|
|
97
97
|
- **Строгая REST валидация и Enum Guard**: Обработчик `POST /dsh-goal/action` валидирует непустой `title` при старте цели и проверяет допустимость статуса вехи по `MilestoneStatus` enum, возвращая внятный HTTP 400 Bad Request / 404 Not Found.
|
|
98
98
|
- **Безопасность файловой системы**: `GoalEngine.writeStateToDiskSync()` гарантированно создаёт отсутствующие родительские директории через `fs.mkdirSync(dir, { recursive: true })` перед созданием временного файла и атомарным переименованием.
|
|
99
|
+
|
|
100
|
+
### Решение 9: Интеллектуальная оценка времени (ETA), аналитика токенов, кнопка быстрого запуска цели и экспорт отчетов в Markdown
|
|
101
|
+
|
|
102
|
+
**Контекст:**
|
|
103
|
+
После стабилизации сессионной изоляции и SSE-потока пользователям требовалась наглядная оценка оставшегося времени работы над целью, понимание расхода токенов за цикл, удобный запуск цели без ручного ввода команды `/goal` и возможность мгновенно экспортировать структурированный итоговый отчёт в markdown-формате.
|
|
104
|
+
|
|
105
|
+
**Принятые решения:**
|
|
106
|
+
1. **Расчет оставшегося времени (ETA projection):**
|
|
107
|
+
- Метод `getEstimatedRemainingSeconds(sid)` рассчитывает среднее время на выполнение завершенных вех `elapsed / completedCount` и умножает на количество оставшихся шагов.
|
|
108
|
+
- В верхнем баннере и модальном окне отображается живое время работы с прогнозом: `⏱ 45s (ETA ~2m)`.
|
|
109
|
+
2. **Аналитика расхода токенов (Token Usage Analytics):**
|
|
110
|
+
- Накопление счетчиков `promptTokens`, `completionTokens`, `totalTokens` на каждом завершении хода `turn/end` через `engine.addTokenUsage(usage, sid)`.
|
|
111
|
+
- В модальном окне деталей в сетку характеристик добавлен блок «Токены» с подсказкой при наведении с детальным расщеплением.
|
|
112
|
+
3. **Кнопка быстрого запуска цели (Quick Launch button):**
|
|
113
|
+
- Когда цель не активна, в `conversation.input.dock` отображается компактная кнопка с иконкой 🎯.
|
|
114
|
+
- По клику открывается модальное окно с полем ввода для немедленной постановки задачи агенту без ручного ввода слэш-команд.
|
|
115
|
+
- В карточке настроек добавлен переключатель `showQuickLaunchButton` с возможностью сброса к дефолту.
|
|
116
|
+
4. **Экспорт отчета в Markdown (One-click Markdown Export):**
|
|
117
|
+
- Для выполненной цели модальное окно предоставляет кнопку «📋 Скопировать отчёт в Markdown».
|
|
118
|
+
- Генерирует отчет с заголовком, статусом, длительностью, итерациями, расходом токенов, резюме результатов и таблицей вех.
|
package/lib/client.js
CHANGED
|
@@ -182,6 +182,32 @@ window.__ModuleLoader__.load({
|
|
|
182
182
|
display: flex;
|
|
183
183
|
justify-content: center;
|
|
184
184
|
}
|
|
185
|
+
.dsh-goal-dock.dsh-goal-dock-quick {
|
|
186
|
+
justify-content: flex-start;
|
|
187
|
+
margin-bottom: 4px;
|
|
188
|
+
}
|
|
189
|
+
.dsh-goal-quicklaunch-btn {
|
|
190
|
+
appearance: none;
|
|
191
|
+
font: inherit;
|
|
192
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
193
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
194
|
+
border-radius: 999px;
|
|
195
|
+
padding: 4px 11px;
|
|
196
|
+
color: var(--dsw-alias-label-secondary);
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 6px;
|
|
203
|
+
transition: all 0.15s ease;
|
|
204
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
205
|
+
}
|
|
206
|
+
.dsh-goal-quicklaunch-btn:hover {
|
|
207
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
208
|
+
border-color: var(--dsw-alias-label-tertiary);
|
|
209
|
+
color: var(--dsw-alias-label-primary);
|
|
210
|
+
}
|
|
185
211
|
.dsh-goal-banner {
|
|
186
212
|
box-sizing: border-box;
|
|
187
213
|
width: 100%;
|
|
@@ -382,7 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
382
408
|
}
|
|
383
409
|
.dsh-goal-stat-grid {
|
|
384
410
|
display: grid;
|
|
385
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
411
|
+
grid-template-columns: repeat(auto-fit, minmax(95px, 1fr));
|
|
386
412
|
gap: 10px;
|
|
387
413
|
}
|
|
388
414
|
.dsh-goal-stat-box {
|
|
@@ -395,13 +421,17 @@ window.__ModuleLoader__.load({
|
|
|
395
421
|
gap: 2px;
|
|
396
422
|
}
|
|
397
423
|
.dsh-goal-stat-val {
|
|
398
|
-
font-size:
|
|
424
|
+
font-size: 15px;
|
|
399
425
|
font-weight: 700;
|
|
400
426
|
color: var(--dsw-alias-label-primary);
|
|
427
|
+
overflow: hidden;
|
|
428
|
+
text-overflow: ellipsis;
|
|
429
|
+
white-space: nowrap;
|
|
401
430
|
}
|
|
402
431
|
.dsh-goal-stat-lbl {
|
|
403
432
|
font-size: 11px;
|
|
404
433
|
color: var(--dsw-alias-label-secondary);
|
|
434
|
+
white-space: nowrap;
|
|
405
435
|
}
|
|
406
436
|
.dsh-goal-milestone-item {
|
|
407
437
|
display: flex;
|
|
@@ -421,6 +451,17 @@ window.__ModuleLoader__.load({
|
|
|
421
451
|
justify-content: flex-end;
|
|
422
452
|
align-items: center;
|
|
423
453
|
gap: 10px;
|
|
454
|
+
flex-wrap: wrap;
|
|
455
|
+
}
|
|
456
|
+
.dsh-goal-btn-copy {
|
|
457
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
458
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
459
|
+
color: var(--dsw-alias-label-primary);
|
|
460
|
+
}
|
|
461
|
+
.dsh-goal-btn-copy.copied {
|
|
462
|
+
background: rgba(16, 185, 129, 0.12);
|
|
463
|
+
border-color: var(--dsw-alias-state-success-primary);
|
|
464
|
+
color: var(--dsw-alias-state-success-primary);
|
|
424
465
|
}
|
|
425
466
|
|
|
426
467
|
/* Settings Card (clinebot pattern) */
|
|
@@ -658,12 +699,77 @@ window.__ModuleLoader__.load({
|
|
|
658
699
|
} catch (_) {}
|
|
659
700
|
}
|
|
660
701
|
|
|
702
|
+
// --- ГЕНЕРАТОР MARKDOWN-ОТЧЕТА ---
|
|
703
|
+
function generateMarkdownReport(state) {
|
|
704
|
+
if (!state) return '';
|
|
705
|
+
const title = state.title || 'Goal Report';
|
|
706
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
707
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
708
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
709
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
710
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
711
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
712
|
+
|
|
713
|
+
let md = `# 🎯 Goal Report: ${title}\n\n`;
|
|
714
|
+
md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`\n`;
|
|
715
|
+
if (totalTokens > 0) {
|
|
716
|
+
md += `**Tokens:** \`${totalTokens.toLocaleString()}\` (Prompt: \`${promptTokens.toLocaleString()}\`, Completion: \`${compTokens.toLocaleString()}\`)\n`;
|
|
717
|
+
}
|
|
718
|
+
md += '\n';
|
|
719
|
+
|
|
720
|
+
if (state.description) {
|
|
721
|
+
md += `### Description\n${state.description}\n\n`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (state.resultSummary) {
|
|
725
|
+
md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const milestones = state.milestones || [];
|
|
729
|
+
if (milestones.length > 0) {
|
|
730
|
+
md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
|
|
731
|
+
milestones.forEach((m, idx) => {
|
|
732
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
733
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
734
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
735
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
736
|
+
});
|
|
737
|
+
md += '\n';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
|
|
741
|
+
return md;
|
|
742
|
+
}
|
|
743
|
+
|
|
661
744
|
// --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
|
|
662
745
|
function GoalDetailsModal({ state, onClose, onAction, t }) {
|
|
663
746
|
if (!state) return null;
|
|
664
747
|
|
|
665
748
|
const milestones = state.milestones || [];
|
|
666
749
|
const isCompleted = state.state === 'COMPLETED';
|
|
750
|
+
const [copied, setCopied] = useState(false);
|
|
751
|
+
|
|
752
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
753
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
754
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
755
|
+
const tokensFormatted = totalTokens > 0
|
|
756
|
+
? (totalTokens >= 1000 ? `${(totalTokens / 1000).toFixed(1)}k` : `${totalTokens}`)
|
|
757
|
+
: '—';
|
|
758
|
+
const tokensTooltip = totalTokens > 0
|
|
759
|
+
? `Prompt: ${promptTokens.toLocaleString()} | Completion: ${compTokens.toLocaleString()} | Total: ${totalTokens.toLocaleString()}`
|
|
760
|
+
: 'Токены ещё не зафиксированы';
|
|
761
|
+
|
|
762
|
+
const handleCopyReport = () => {
|
|
763
|
+
const md = generateMarkdownReport(state);
|
|
764
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
765
|
+
navigator.clipboard.writeText(md).then(() => {
|
|
766
|
+
setCopied(true);
|
|
767
|
+
setTimeout(() => setCopied(false), 2500);
|
|
768
|
+
}).catch((err) => {
|
|
769
|
+
console.warn('[dsh-goal] Clipboard copy failed:', err);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
};
|
|
667
773
|
|
|
668
774
|
return React.createElement(
|
|
669
775
|
'div',
|
|
@@ -713,6 +819,18 @@ window.__ModuleLoader__.load({
|
|
|
713
819
|
React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedElapsed || '0s'),
|
|
714
820
|
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('time') || 'Время работы'),
|
|
715
821
|
),
|
|
822
|
+
React.createElement(
|
|
823
|
+
'div',
|
|
824
|
+
{ className: 'dsh-goal-stat-box' },
|
|
825
|
+
React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedETA || (isCompleted ? 'Готово' : '—')),
|
|
826
|
+
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('eta') || 'Оценка (ETA)'),
|
|
827
|
+
),
|
|
828
|
+
React.createElement(
|
|
829
|
+
'div',
|
|
830
|
+
{ className: 'dsh-goal-stat-box', title: tokensTooltip },
|
|
831
|
+
React.createElement('span', { className: 'dsh-goal-stat-val' }, tokensFormatted),
|
|
832
|
+
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('tokens') || 'Токены'),
|
|
833
|
+
),
|
|
716
834
|
React.createElement(
|
|
717
835
|
'div',
|
|
718
836
|
{ className: 'dsh-goal-stat-box' },
|
|
@@ -760,6 +878,17 @@ window.__ModuleLoader__.load({
|
|
|
760
878
|
React.createElement(
|
|
761
879
|
'div',
|
|
762
880
|
{ className: 'dsh-goal-modal-foot' },
|
|
881
|
+
isCompleted
|
|
882
|
+
? React.createElement(
|
|
883
|
+
'button',
|
|
884
|
+
{
|
|
885
|
+
className: `dsh-goal-btn dsh-goal-btn-copy${copied ? ' copied' : ''}`,
|
|
886
|
+
onClick: handleCopyReport,
|
|
887
|
+
title: t('copyReport') || 'Скопировать отчёт в Markdown',
|
|
888
|
+
},
|
|
889
|
+
copied ? (t('reportCopied') || '✅ Скопировано!') : (t('copyReport') || '📋 Скопировать отчёт в Markdown'),
|
|
890
|
+
)
|
|
891
|
+
: null,
|
|
763
892
|
isCompleted
|
|
764
893
|
? React.createElement(
|
|
765
894
|
'button',
|
|
@@ -786,6 +915,98 @@ window.__ModuleLoader__.load({
|
|
|
786
915
|
);
|
|
787
916
|
}
|
|
788
917
|
|
|
918
|
+
// --- МОДАЛЬНОЕ ОКНО БЫСТРОГО ЗАПУСКА ЦЕЛИ (QUICK LAUNCH MODAL) ---
|
|
919
|
+
function QuickLaunchModal({ onClose, onStart, t }) {
|
|
920
|
+
const [title, setTitle] = useState('');
|
|
921
|
+
const inputRef = useRef(null);
|
|
922
|
+
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
if (inputRef.current) inputRef.current.focus();
|
|
925
|
+
}, []);
|
|
926
|
+
|
|
927
|
+
const handleSubmit = (e) => {
|
|
928
|
+
if (e) e.preventDefault();
|
|
929
|
+
const trimmed = title.trim();
|
|
930
|
+
if (trimmed) {
|
|
931
|
+
onStart(trimmed);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
return React.createElement(
|
|
936
|
+
'div',
|
|
937
|
+
{ className: 'dsh-goal-modal-overlay', onClick: onClose },
|
|
938
|
+
React.createElement(
|
|
939
|
+
'div',
|
|
940
|
+
{
|
|
941
|
+
className: 'dsh-goal-modal',
|
|
942
|
+
style: { maxWidth: 480 },
|
|
943
|
+
onClick: (e) => e.stopPropagation(),
|
|
944
|
+
},
|
|
945
|
+
React.createElement(
|
|
946
|
+
'div',
|
|
947
|
+
{ className: 'dsh-goal-modal-head' },
|
|
948
|
+
React.createElement(
|
|
949
|
+
'div',
|
|
950
|
+
{ className: 'dsh-goal-modal-title' },
|
|
951
|
+
React.createElement(IconTarget, { size: 18 }),
|
|
952
|
+
t('quickLaunchTitle') || 'Быстрый запуск цели',
|
|
953
|
+
),
|
|
954
|
+
React.createElement(
|
|
955
|
+
'button',
|
|
956
|
+
{ className: 'dsh-goal-btn icon-only close', onClick: onClose, title: t('close') || 'Закрыть' },
|
|
957
|
+
'✕',
|
|
958
|
+
),
|
|
959
|
+
),
|
|
960
|
+
React.createElement(
|
|
961
|
+
'form',
|
|
962
|
+
{ onSubmit: handleSubmit },
|
|
963
|
+
React.createElement(
|
|
964
|
+
'div',
|
|
965
|
+
{ className: 'dsh-goal-modal-body' },
|
|
966
|
+
React.createElement(
|
|
967
|
+
'div',
|
|
968
|
+
{ style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)', marginBottom: 8 } },
|
|
969
|
+
t('quickLaunchDesc') || 'Сформулируйте задачу для агента в автономном режиме:',
|
|
970
|
+
),
|
|
971
|
+
React.createElement('input', {
|
|
972
|
+
ref: inputRef,
|
|
973
|
+
type: 'text',
|
|
974
|
+
className: 'dsh-goal-card-input',
|
|
975
|
+
style: { width: '100%', boxSizing: 'border-box', fontSize: 13, padding: '8px 10px' },
|
|
976
|
+
placeholder: t('quickLaunchPlaceholder') || 'Например: Реализовать экспорт отчетов и покрыть тестами...',
|
|
977
|
+
value: title,
|
|
978
|
+
onChange: (e) => setTitle(e.target.value),
|
|
979
|
+
onKeyDown: (e) => {
|
|
980
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
981
|
+
e.preventDefault();
|
|
982
|
+
handleSubmit();
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
}),
|
|
986
|
+
),
|
|
987
|
+
React.createElement(
|
|
988
|
+
'div',
|
|
989
|
+
{ className: 'dsh-goal-modal-foot' },
|
|
990
|
+
React.createElement(
|
|
991
|
+
'button',
|
|
992
|
+
{ type: 'button', className: 'dsh-goal-btn', onClick: onClose },
|
|
993
|
+
t('close') || 'Отмена',
|
|
994
|
+
),
|
|
995
|
+
React.createElement(
|
|
996
|
+
'button',
|
|
997
|
+
{
|
|
998
|
+
type: 'submit',
|
|
999
|
+
className: 'dsh-goal-btn dsh-goal-btn-primary',
|
|
1000
|
+
disabled: !title.trim(),
|
|
1001
|
+
},
|
|
1002
|
+
t('startGoal') || 'Запустить цель',
|
|
1003
|
+
),
|
|
1004
|
+
),
|
|
1005
|
+
),
|
|
1006
|
+
),
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
789
1010
|
function sessionIdOf(ctx, props) {
|
|
790
1011
|
try {
|
|
791
1012
|
if (props?.session?.id) return String(props.session.id);
|
|
@@ -809,6 +1030,7 @@ window.__ModuleLoader__.load({
|
|
|
809
1030
|
const sid = sessionIdOf(ctx, props);
|
|
810
1031
|
const [state, setState] = useState(null);
|
|
811
1032
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
1033
|
+
const [isQuickLaunchOpen, setIsQuickLaunchOpen] = useState(false);
|
|
812
1034
|
const [now, setNow] = useState(() => Date.now());
|
|
813
1035
|
const stateRef = useRef(null);
|
|
814
1036
|
stateRef.current = state;
|
|
@@ -1018,8 +1240,38 @@ window.__ModuleLoader__.load({
|
|
|
1018
1240
|
} catch (_) {}
|
|
1019
1241
|
};
|
|
1020
1242
|
|
|
1243
|
+
const showQuickLaunch = state ? (state.showQuickLaunchButton !== false) : true;
|
|
1244
|
+
|
|
1021
1245
|
if (!state || !state.hasActiveGoal) {
|
|
1022
|
-
return null;
|
|
1246
|
+
if (!showQuickLaunch) return null;
|
|
1247
|
+
return React.createElement(
|
|
1248
|
+
React.Fragment,
|
|
1249
|
+
null,
|
|
1250
|
+
React.createElement(
|
|
1251
|
+
'div',
|
|
1252
|
+
{ className: 'dsh-goal-dock dsh-goal-dock-quick', 'data-goal-dock': 'true' },
|
|
1253
|
+
React.createElement(
|
|
1254
|
+
'button',
|
|
1255
|
+
{
|
|
1256
|
+
className: 'dsh-goal-quicklaunch-btn',
|
|
1257
|
+
title: t('quickLaunch') || 'Запустить цель (Goal Mode)',
|
|
1258
|
+
onClick: () => setIsQuickLaunchOpen(true),
|
|
1259
|
+
},
|
|
1260
|
+
React.createElement(IconTarget, { size: 14 }),
|
|
1261
|
+
React.createElement('span', null, t('quickLaunch') || 'Запустить цель'),
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
isQuickLaunchOpen
|
|
1265
|
+
? React.createElement(QuickLaunchModal, {
|
|
1266
|
+
onClose: () => setIsQuickLaunchOpen(false),
|
|
1267
|
+
onStart: async (title) => {
|
|
1268
|
+
await handleAction('start', { title });
|
|
1269
|
+
setIsQuickLaunchOpen(false);
|
|
1270
|
+
},
|
|
1271
|
+
t,
|
|
1272
|
+
})
|
|
1273
|
+
: null,
|
|
1274
|
+
);
|
|
1023
1275
|
}
|
|
1024
1276
|
|
|
1025
1277
|
const isPaused = state.state === 'PAUSED';
|
|
@@ -1034,6 +1286,7 @@ window.__ModuleLoader__.load({
|
|
|
1034
1286
|
};
|
|
1035
1287
|
|
|
1036
1288
|
const liveElapsedStr = formatLiveElapsed();
|
|
1289
|
+
const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
|
|
1037
1290
|
|
|
1038
1291
|
return React.createElement(
|
|
1039
1292
|
React.Fragment,
|
|
@@ -1067,7 +1320,7 @@ window.__ModuleLoader__.load({
|
|
|
1067
1320
|
React.createElement(
|
|
1068
1321
|
'span',
|
|
1069
1322
|
{ className: 'dsh-goal-time' },
|
|
1070
|
-
`⏱ ${liveElapsedStr}`,
|
|
1323
|
+
`⏱ ${liveElapsedStr}${etaStr}`,
|
|
1071
1324
|
),
|
|
1072
1325
|
!isCompleted
|
|
1073
1326
|
? React.createElement(
|
|
@@ -1119,6 +1372,7 @@ window.__ModuleLoader__.load({
|
|
|
1119
1372
|
maxIterations: 25,
|
|
1120
1373
|
autoDrive: true,
|
|
1121
1374
|
enableSound: true,
|
|
1375
|
+
showQuickLaunchButton: true,
|
|
1122
1376
|
};
|
|
1123
1377
|
|
|
1124
1378
|
function parseNumberField(raw) {
|
|
@@ -1177,7 +1431,7 @@ window.__ModuleLoader__.load({
|
|
|
1177
1431
|
function computeSavePlan(draft, snap) {
|
|
1178
1432
|
if (!draft) return [];
|
|
1179
1433
|
|
|
1180
|
-
const fields = ['maxIterations', 'autoDrive', 'enableSound'];
|
|
1434
|
+
const fields = ['maxIterations', 'autoDrive', 'enableSound', 'showQuickLaunchButton'];
|
|
1181
1435
|
const writes = [];
|
|
1182
1436
|
|
|
1183
1437
|
for (const f of fields) {
|
|
@@ -1234,8 +1488,9 @@ window.__ModuleLoader__.load({
|
|
|
1234
1488
|
const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
|
|
1235
1489
|
const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
|
|
1236
1490
|
const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
|
|
1491
|
+
const quickLaunchStatus = getFieldStatus('showQuickLaunchButton', draft?.showQuickLaunchButton, snap);
|
|
1237
1492
|
|
|
1238
|
-
const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
|
|
1493
|
+
const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty || quickLaunchStatus.isDirty;
|
|
1239
1494
|
const invalid = maxIterStatus.invalid;
|
|
1240
1495
|
const disabled = !scope || saving || snap?.writable === false;
|
|
1241
1496
|
|
|
@@ -1245,6 +1500,7 @@ window.__ModuleLoader__.load({
|
|
|
1245
1500
|
maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
|
|
1246
1501
|
autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
|
|
1247
1502
|
enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
|
|
1503
|
+
showQuickLaunchButton: prev?.showQuickLaunchButton !== undefined ? prev.showQuickLaunchButton : quickLaunchStatus.value,
|
|
1248
1504
|
[field]: val,
|
|
1249
1505
|
}));
|
|
1250
1506
|
};
|
|
@@ -1418,6 +1674,33 @@ window.__ModuleLoader__.load({
|
|
|
1418
1674
|
)
|
|
1419
1675
|
: null,
|
|
1420
1676
|
),
|
|
1677
|
+
React.createElement(
|
|
1678
|
+
'div',
|
|
1679
|
+
{ className: 'dsh-goal-check-row' },
|
|
1680
|
+
React.createElement(
|
|
1681
|
+
'label',
|
|
1682
|
+
{ className: 'dsh-goal-check' },
|
|
1683
|
+
React.createElement('input', {
|
|
1684
|
+
type: 'checkbox',
|
|
1685
|
+
checked: quickLaunchStatus.value !== false,
|
|
1686
|
+
disabled,
|
|
1687
|
+
onChange: (e) => edit('showQuickLaunchButton', e.target.checked),
|
|
1688
|
+
}),
|
|
1689
|
+
t('quickLaunchLabel') || 'Кнопка быстрого запуска цели над полем ввода',
|
|
1690
|
+
),
|
|
1691
|
+
quickLaunchStatus.isOverridden
|
|
1692
|
+
? React.createElement(
|
|
1693
|
+
'button',
|
|
1694
|
+
{
|
|
1695
|
+
type: 'button',
|
|
1696
|
+
className: 'dsh-goal-btn-inline-reset',
|
|
1697
|
+
title: t('resetField') || 'Сбросить к значению по умолчанию',
|
|
1698
|
+
onClick: () => resetFieldToDefault('showQuickLaunchButton'),
|
|
1699
|
+
},
|
|
1700
|
+
React.createElement(IconRotateCcw, { size: 12 }),
|
|
1701
|
+
)
|
|
1702
|
+
: null,
|
|
1703
|
+
),
|
|
1421
1704
|
React.createElement(
|
|
1422
1705
|
'div',
|
|
1423
1706
|
{ className: 'dsh-goal-foot' },
|
|
@@ -1461,6 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1461
1744
|
modalTitle: 'План и статус цели',
|
|
1462
1745
|
modalTitleCompleted: 'Цель выполнена: план и результаты',
|
|
1463
1746
|
time: 'Время работы',
|
|
1747
|
+
eta: 'Оценка (ETA)',
|
|
1748
|
+
tokens: 'Токены',
|
|
1464
1749
|
milestones: 'План работ:',
|
|
1465
1750
|
noMilestones: 'Агент формирует план работ...',
|
|
1466
1751
|
close: 'Закрыть',
|
|
@@ -1469,6 +1754,14 @@ window.__ModuleLoader__.load({
|
|
|
1469
1754
|
maxIterLabel: 'Максимум итераций (Safety Limit):',
|
|
1470
1755
|
autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
|
|
1471
1756
|
soundLabel: 'Звук по завершении цели',
|
|
1757
|
+
quickLaunchLabel: 'Кнопка быстрого запуска цели над полем ввода',
|
|
1758
|
+
quickLaunch: 'Запустить цель',
|
|
1759
|
+
quickLaunchTitle: 'Быстрый запуск цели',
|
|
1760
|
+
quickLaunchDesc: 'Сформулируйте задачу для агента в автономном режиме:',
|
|
1761
|
+
quickLaunchPlaceholder: 'Например: Реализовать экспорт отчетов и покрыть тестами...',
|
|
1762
|
+
startGoal: 'Запустить цель',
|
|
1763
|
+
copyReport: '📋 Скопировать отчёт в Markdown',
|
|
1764
|
+
reportCopied: '✅ Скопировано!',
|
|
1472
1765
|
save: 'Сохранить',
|
|
1473
1766
|
saving: 'Сохранение…',
|
|
1474
1767
|
discard: 'Отменить правки',
|
|
@@ -1489,6 +1782,8 @@ window.__ModuleLoader__.load({
|
|
|
1489
1782
|
modalTitle: 'Goal Plan & Status',
|
|
1490
1783
|
modalTitleCompleted: 'Goal Completed: Plan & Results',
|
|
1491
1784
|
time: 'Running time',
|
|
1785
|
+
eta: 'ETA',
|
|
1786
|
+
tokens: 'Tokens',
|
|
1492
1787
|
milestones: 'Plan of work:',
|
|
1493
1788
|
noMilestones: 'Agent is preparing the plan of work...',
|
|
1494
1789
|
close: 'Close',
|
|
@@ -1497,6 +1792,14 @@ window.__ModuleLoader__.load({
|
|
|
1497
1792
|
maxIterLabel: 'Max iterations (Safety Limit):',
|
|
1498
1793
|
autoDriveLabel: 'Auto-drive: keep the loop running automatically',
|
|
1499
1794
|
soundLabel: 'Sound when a goal completes',
|
|
1795
|
+
quickLaunchLabel: 'Quick launch goal button above composer dock',
|
|
1796
|
+
quickLaunch: 'Start Goal',
|
|
1797
|
+
quickLaunchTitle: 'Quick Launch Goal',
|
|
1798
|
+
quickLaunchDesc: 'Define the objective for the agent in autonomous mode:',
|
|
1799
|
+
quickLaunchPlaceholder: 'e.g. Implement report export and cover with unit tests...',
|
|
1800
|
+
startGoal: 'Start Goal',
|
|
1801
|
+
copyReport: '📋 Copy Report in Markdown',
|
|
1802
|
+
reportCopied: '✅ Copied!',
|
|
1500
1803
|
save: 'Save',
|
|
1501
1804
|
saving: 'Saving…',
|
|
1502
1805
|
discard: 'Discard changes',
|
package/lib/command-handler.js
CHANGED
|
@@ -76,12 +76,18 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
|
|
|
76
76
|
? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
77
77
|
: '';
|
|
78
78
|
|
|
79
|
+
const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
|
|
80
|
+
const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
|
|
81
|
+
? `\nТокены: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`
|
|
82
|
+
: '';
|
|
83
|
+
|
|
79
84
|
return {
|
|
80
85
|
kind: 'success',
|
|
81
86
|
text: `🎯 Цель: «${snap.title}»\n` +
|
|
82
87
|
`Статус: ${snap.state}\n` +
|
|
83
|
-
`Время: ${snap.formattedElapsed}\n` +
|
|
88
|
+
`Время: ${snap.formattedElapsed}${etaText}\n` +
|
|
84
89
|
`Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
90
|
+
tokensInfo +
|
|
85
91
|
milestonesInfo +
|
|
86
92
|
`\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`,
|
|
87
93
|
};
|
package/lib/goal-engine.js
CHANGED
|
@@ -42,11 +42,28 @@ export function formatElapsed(totalSeconds) {
|
|
|
42
42
|
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Форматирование расчетного оставшегося времени (ETA)
|
|
47
|
+
* @param {number|null} seconds
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
export function formatETA(seconds) {
|
|
51
|
+
if (seconds == null || isNaN(seconds)) return null;
|
|
52
|
+
const sec = Math.max(0, Math.floor(seconds));
|
|
53
|
+
if (sec < 60) return `~${sec}s`;
|
|
54
|
+
const mins = Math.round(sec / 60);
|
|
55
|
+
if (mins < 60) return `~${mins}m`;
|
|
56
|
+
const hours = Math.floor(mins / 60);
|
|
57
|
+
const remainingMins = mins % 60;
|
|
58
|
+
return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
export class GoalEngine {
|
|
46
62
|
constructor(options = {}) {
|
|
47
63
|
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
64
|
this.autoDrive = options.autoDrive ?? true;
|
|
49
65
|
this.enableSound = options.enableSound ?? true;
|
|
66
|
+
this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
|
|
50
67
|
this.maxSessions = options.maxSessions ?? 100;
|
|
51
68
|
this.goals = new Map();
|
|
52
69
|
this.listeners = new Set();
|
|
@@ -96,6 +113,9 @@ export class GoalEngine {
|
|
|
96
113
|
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
97
114
|
dirty = true;
|
|
98
115
|
}
|
|
116
|
+
if (!goal.tokensUsage) {
|
|
117
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
118
|
+
}
|
|
99
119
|
this.goals.set(sid, goal);
|
|
100
120
|
}
|
|
101
121
|
}
|
|
@@ -112,6 +132,9 @@ export class GoalEngine {
|
|
|
112
132
|
if (data.logs.length > 100) data.logs = data.logs.slice(-100);
|
|
113
133
|
dirty = true;
|
|
114
134
|
}
|
|
135
|
+
if (!data.tokensUsage) {
|
|
136
|
+
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
137
|
+
}
|
|
115
138
|
this.goals.set('default', data);
|
|
116
139
|
}
|
|
117
140
|
if (dirty) {
|
|
@@ -251,6 +274,9 @@ export class GoalEngine {
|
|
|
251
274
|
if (typeof config.enableSound === 'boolean') {
|
|
252
275
|
this.enableSound = config.enableSound;
|
|
253
276
|
}
|
|
277
|
+
if (typeof config.showQuickLaunchButton === 'boolean') {
|
|
278
|
+
this.showQuickLaunchButton = config.showQuickLaunchButton;
|
|
279
|
+
}
|
|
254
280
|
this.emit();
|
|
255
281
|
}
|
|
256
282
|
|
|
@@ -312,6 +338,7 @@ export class GoalEngine {
|
|
|
312
338
|
iterationsCount: 0,
|
|
313
339
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
314
340
|
milestones: [],
|
|
341
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
315
342
|
logs: [
|
|
316
343
|
{
|
|
317
344
|
timestamp: now,
|
|
@@ -561,6 +588,57 @@ export class GoalEngine {
|
|
|
561
588
|
return Math.floor(elapsedMs / 1000);
|
|
562
589
|
}
|
|
563
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Накопление статистики использования токенов сессии
|
|
593
|
+
* @param {Object} usage
|
|
594
|
+
* @param {string} [sessionId='default']
|
|
595
|
+
*/
|
|
596
|
+
addTokenUsage(usage, sessionId = 'default') {
|
|
597
|
+
if (!usage || typeof usage !== 'object') return;
|
|
598
|
+
const sid = sessionId || 'default';
|
|
599
|
+
const goal = this.goals.get(sid);
|
|
600
|
+
if (!goal) return;
|
|
601
|
+
|
|
602
|
+
if (!goal.tokensUsage) {
|
|
603
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
607
|
+
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
608
|
+
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
609
|
+
|
|
610
|
+
goal.tokensUsage.promptTokens += prompt;
|
|
611
|
+
goal.tokensUsage.completionTokens += completion;
|
|
612
|
+
goal.tokensUsage.totalTokens += total;
|
|
613
|
+
|
|
614
|
+
this.emit(sid);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Интеллектуальный расчет прогноза оставшегося времени (ETA)
|
|
619
|
+
* на основе средней скорости выполнения завершенных вех
|
|
620
|
+
* @param {string} [sessionId='default']
|
|
621
|
+
* @returns {number|null}
|
|
622
|
+
*/
|
|
623
|
+
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
624
|
+
const sid = sessionId || 'default';
|
|
625
|
+
const goal = this.goals.get(sid);
|
|
626
|
+
if (!goal || goal.state !== GoalState.RUNNING) return null;
|
|
627
|
+
|
|
628
|
+
const total = goal.milestones.length;
|
|
629
|
+
if (total === 0) return null;
|
|
630
|
+
|
|
631
|
+
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
632
|
+
if (completedCount === 0 || completedCount >= total) return null;
|
|
633
|
+
|
|
634
|
+
const elapsed = this.getElapsedSeconds(sid);
|
|
635
|
+
if (elapsed <= 0) return null;
|
|
636
|
+
|
|
637
|
+
const avgSecPerMilestone = elapsed / completedCount;
|
|
638
|
+
const remainingCount = total - completedCount;
|
|
639
|
+
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
640
|
+
}
|
|
641
|
+
|
|
564
642
|
/**
|
|
565
643
|
* Снимок состояния для передачи клиенту / API
|
|
566
644
|
*/
|
|
@@ -579,12 +657,16 @@ export class GoalEngine {
|
|
|
579
657
|
completedAt: null,
|
|
580
658
|
elapsedSeconds: 0,
|
|
581
659
|
formattedElapsed: '0s',
|
|
660
|
+
estimatedRemainingSeconds: null,
|
|
661
|
+
formattedETA: null,
|
|
662
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
582
663
|
milestones: [],
|
|
583
664
|
progressPercent: 0,
|
|
584
665
|
iterationsCount: 0,
|
|
585
666
|
maxIterations: this.defaultMaxIterations,
|
|
586
667
|
autoDrive: this.autoDrive,
|
|
587
668
|
enableSound: this.enableSound,
|
|
669
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
588
670
|
};
|
|
589
671
|
}
|
|
590
672
|
|
|
@@ -592,6 +674,7 @@ export class GoalEngine {
|
|
|
592
674
|
const milestones = goal.milestones;
|
|
593
675
|
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
594
676
|
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
677
|
+
const estSec = this.getEstimatedRemainingSeconds(sid);
|
|
595
678
|
|
|
596
679
|
return {
|
|
597
680
|
sessionId: sid,
|
|
@@ -606,6 +689,9 @@ export class GoalEngine {
|
|
|
606
689
|
completedAt: goal.completedAt,
|
|
607
690
|
elapsedSeconds: elapsed,
|
|
608
691
|
formattedElapsed: formatElapsed(elapsed),
|
|
692
|
+
estimatedRemainingSeconds: estSec,
|
|
693
|
+
formattedETA: formatETA(estSec),
|
|
694
|
+
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
609
695
|
iterationsCount: goal.iterationsCount,
|
|
610
696
|
maxIterations: goal.maxIterations,
|
|
611
697
|
milestones,
|
|
@@ -614,6 +700,7 @@ export class GoalEngine {
|
|
|
614
700
|
resultSummary: goal.resultSummary,
|
|
615
701
|
autoDrive: this.autoDrive,
|
|
616
702
|
enableSound: this.enableSound,
|
|
703
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
617
704
|
};
|
|
618
705
|
}
|
|
619
706
|
|
|
@@ -633,9 +720,11 @@ export class GoalEngine {
|
|
|
633
720
|
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
634
721
|
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
635
722
|
|
|
723
|
+
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
724
|
+
|
|
636
725
|
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
637
726
|
Цель: "${snapshot.title}"
|
|
638
|
-
Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
727
|
+
Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
639
728
|
План работ:
|
|
640
729
|
${milestonesText}
|
|
641
730
|
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export const Config = z.object({
|
|
|
42
42
|
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
43
43
|
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
44
44
|
enableSound: z.boolean().default(true).description('Play synthesized audio chime on goal completion or failure'),
|
|
45
|
+
showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
|
|
45
46
|
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -59,6 +60,7 @@ export function apply(ctx, config = {}) {
|
|
|
59
60
|
maxIterations: config?.maxIterations ?? 25,
|
|
60
61
|
autoDrive: config?.autoDrive ?? true,
|
|
61
62
|
enableSound: config?.enableSound ?? true,
|
|
63
|
+
showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
|
|
62
64
|
storagePath: config?.storagePath,
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -71,6 +73,7 @@ export function apply(ctx, config = {}) {
|
|
|
71
73
|
defaultMaxIterations: currentSettings.maxIterations,
|
|
72
74
|
autoDrive: currentSettings.autoDrive,
|
|
73
75
|
enableSound: currentSettings.enableSound,
|
|
76
|
+
showQuickLaunchButton: currentSettings.showQuickLaunchButton,
|
|
74
77
|
storagePath,
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -215,6 +218,7 @@ export function apply(ctx, config = {}) {
|
|
|
215
218
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
216
219
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
217
220
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
221
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
218
222
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
219
223
|
};
|
|
220
224
|
}
|
|
@@ -226,6 +230,7 @@ export function apply(ctx, config = {}) {
|
|
|
226
230
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
227
231
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
228
232
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
233
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
229
234
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
230
235
|
};
|
|
231
236
|
}
|
|
@@ -240,6 +245,7 @@ export function apply(ctx, config = {}) {
|
|
|
240
245
|
defaultMaxIterations: live.maxIterations,
|
|
241
246
|
autoDrive: live.autoDrive,
|
|
242
247
|
enableSound: live.enableSound,
|
|
248
|
+
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
243
249
|
});
|
|
244
250
|
};
|
|
245
251
|
|
|
@@ -536,6 +542,7 @@ export function apply(ctx, config = {}) {
|
|
|
536
542
|
description: typeof description === 'string' ? description.trim() : '',
|
|
537
543
|
maxIterations: getConfig().maxIterations,
|
|
538
544
|
}, sid);
|
|
545
|
+
resumeActiveAgent(`🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`, sid);
|
|
539
546
|
break;
|
|
540
547
|
}
|
|
541
548
|
case 'pause':
|
|
@@ -615,6 +622,13 @@ export function apply(ctx, config = {}) {
|
|
|
615
622
|
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
616
623
|
|
|
617
624
|
const sid = sessionIdOf(turn, 'default');
|
|
625
|
+
|
|
626
|
+
// Накопление токенов по завершении хода
|
|
627
|
+
const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
|
|
628
|
+
if (usage) {
|
|
629
|
+
engine.addTokenUsage(usage, sid);
|
|
630
|
+
}
|
|
631
|
+
|
|
618
632
|
const snap = engine.getSnapshot(sid);
|
|
619
633
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
620
634
|
// Проверяем причину завершения хода
|