@goodandready/dsh-cron 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GooDAnDReaDY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @goodandready/dsh-cron
2
+
3
+ Плагин планировщика и фоновой автоматизации для DeepSeek Harness. Позволяет запускать агентские сессии и автоматические сценарии по расписанию через cron-выражения или интервалы, а также управлять ими через графический оверлей, команду `/cron` и инструмент вызова для модели (Tool Calling).
4
+
5
+ ## Возможности
6
+
7
+ - **Визуальный интерфейс «Запланированные задачи»**:
8
+ - Открытие из бокового меню DSH (кнопка рядом с канбаном).
9
+ - Полноэкранный оверлей со списком активных и приостановленных задач.
10
+ - Поиск и фильтрация по табам: «Все», «Активные», «Приостановленные», «Завершено».
11
+ - Выпадающее меню «Создать ⌄»:
12
+ - 💬 **Создать с DSH** (интерактивная постановка задачи агенту в чате).
13
+ - ✏️ **Настроить вручную** (модальное окно с выбором параметров расписания, промпта и режима).
14
+ - Готовые рекомендации в 1 клик («Ежедневная сводка», «Еженедельный обзор», «Мониторинг дальнейших действий»).
15
+ - Быстрые действия: запуск немедленно («Запустить сейчас»), пауза/возобновление, удаление.
16
+ - **Команда `/cron` в чате**:
17
+ - `/cron list` — вывод списка активных задач.
18
+ - `/cron add "<расписание>" <промпт>` — быстрое создание задачи.
19
+ - `/cron pause <id>` / `/cron resume <id>` — пауза и возобновление.
20
+ - `/cron run <id>` — немедленный запуск.
21
+ - **Инструменты модели (AI Tool Calling)**:
22
+ - `cron_schedule_task` — агент может сам запланировать задачу в ходе диалога.
23
+ - `cron_list_tasks` — просмотр задач.
24
+ - `cron_toggle_task` — управление жизненным циклом.
25
+ - **Движок планировщика**:
26
+ - Построен на базе `croner` с поддержкой выражений стандартного cron (`0 9 * * 1-5`) и интервалов (`every 15m`, `daily`, `weekdays`).
27
+ - Персистентное хранение состояния в JSON с атомарной записью.
28
+ - Журналирование запусков и сохранение истории выполнения (длительность, статус, вывод).
29
+
30
+ ## Установка
31
+
32
+ ```bash
33
+ dsh plugin --profile web add @goodandready/dsh-cron@0.1.0
34
+ ```
35
+
36
+ ## Разработка и тестирование
37
+
38
+ ```bash
39
+ # Запуск юнит-тестов
40
+ node --test test/*.test.mjs
41
+ ```
42
+
43
+ ## Лицензия
44
+
45
+ MIT
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: dsh-cron
3
+ name: '@goodandready/dsh-cron'
4
+ config: {}
@@ -0,0 +1,61 @@
1
+ # DESIGN.md — dsh-cron
2
+
3
+ ## Product / Purpose
4
+ - Назначение: Интегрированный планировщик cron-задач и фоновой автоматизации для DeepSeek Harness. Позволяет запускать агентские сессии по расписанию, выполнять автоматические проверки и предоставлять визуальный интерфейс управления задачами.
5
+ - Аудитория: Пользователи и операторы DeepSeek Harness, автоматизирующие периодические процессы (утренние сводки, проверка тикетов, мониторинг серверов).
6
+ - Статус: In Development, v0.1.0, публичный npm-пакет @goodandready/dsh-cron.
7
+
8
+ ## User Surfaces
9
+ - Web/UI:
10
+ - Экран «Запланированные задачи» (полноэкранный оверлей в центральной колонке интерфейса DSH, аналогично dsh-kanban через mountOverlay/mountBoard).
11
+ - Кнопка вызова в боковой панели (sidebar-entry) рядом с новой сессией и канбаном.
12
+ - Кнопка «Создать ⌄» в правом верхнем углу с дропдауном:
13
+ - 💬 «Создать с DSH» (запуск интерактивного диалога постановки задачи агенту).
14
+ - ✏️ «Настроить вручную» (модальное окно с формой параметров расписания, промпта, типа запуска).
15
+ - Табы фильтрации: «Все», «Активные», «Приостановленные», «Завершено».
16
+ - Поисковая строка: «Поиск запланированных задач».
17
+ - Карточки задач: статус-иконка (play/pause/done), название, расписание (человекочитаемое + raw cron), контекстные действия (пауза/старт, запуск сейчас, редактировать, удалить, логи).
18
+ - Блок «Рекомендации»: готовые шаблоны (Ежедневная сводка, Еженедельный обзор, Мониторинг) в один клик.
19
+ - Карточка настроек в слоте settings.plugin.item (namespace dsh-cron).
20
+ - Chat / Slash Commands:
21
+ - Команда /cron для быстрого создания или просмотра статуса запланированных задач прямо в чате.
22
+ - LLM Tools: cron_schedule_task, cron_list_tasks, cron_toggle_task, cron_get_history.
23
+ - API: HTTP эндпоинты /dsh-cron/* (tasks, toggle, run-now, history, templates).
24
+
25
+ ## Visual Direction
26
+ - Атмосфера: Строгий утилитарный темный/светлый интерфейс в нативном стиле DeepSeek Harness (дизайн как на утвержденных скриншотах пользователя).
27
+ - Использование нативных токенов темы DSH: --dsw-alias-bg-layer-*, --dsw-alias-border-l2, --dsw-alias-label-primary, --dsw-alias-label-secondary.
28
+ - Иерархия: читаемые карточки с четким акцентом на статусе и времени следующего запуска.
29
+
30
+ ## Foundations
31
+ - Семантические цвета статусов:
32
+ - Зеленый/активный: рабочее расписание.
33
+ - Серый/приостановленный: на паузе.
34
+ - Синий/информационный: шаблоны и рекомендации.
35
+ - Красный: ошибка выполнения последнего запуска.
36
+ - Доступность: aria-label для всех иконок, управление клавиатурой в модальных окнах, семантические формы.
37
+
38
+ ## Components And States
39
+ - Components:
40
+ - CronSidebarButton: кнопка в левом сайдбаре DSH.
41
+ - CronScreen: основной оверлей со списком, табами и рекомендациями.
42
+ - CreateDropdown: всплывающее меню выбора способа создания.
43
+ - ManualTaskModal: модальная форма создания/редактирования.
44
+ - TaskItem: строка задачи с переключателем состояния и меню действий.
45
+ - RecommendationCard: плашка с готовым шаблоном.
46
+ - CronSettingsCard: карточка параметров плагина в настройках.
47
+ - States:
48
+ - Loading: скелетон списка при загрузке.
49
+ - Empty: дружелюбный пустой экран с предложением создать первую задачу или выбрать из рекомендаций.
50
+ - Error: баннер с ошибкой и кнопкой повтора.
51
+
52
+ ## User Flows
53
+ 1. Создание через DSH-чат: пользователь пишет «напоминай каждый день в 9 утра...» -> агент вызывает тул cron_schedule_task -> задача сохраняется и появляется на экране.
54
+ 2. Создание через команду: пользователь вводит /cron add 0 9 * * 1-5 <промпт>.
55
+ 3. Создание вручную: клик по кнопке сайдбара -> клик «Создать ⌄» -> «Настроить вручную» -> заполнение формы -> сохранение.
56
+ 4. Выполнение по крону: срабатывает таймер croner -> запуск сессии агента или команды -> запись в историю -> доставка результата.
57
+
58
+ ## Locked Design Decisions
59
+ - 2026-09-03 — Публичный скоуп @goodandready/dsh-cron.
60
+ - 2026-09-03 — Оверлей открывается через mountSidebarEntry и mountScreen аналогично dsh-kanban.
61
+ - 2026-09-03 — Двойная кнопка «Создать ⌄» («Создать с DSH» и «Настроить вручную»).
package/lib/client.js ADDED
@@ -0,0 +1,442 @@
1
+ // Браузерная половина @goodandready/dsh-cron
2
+ window.__ModuleLoader__.load({
3
+ id: '@goodandready/dsh-cron',
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ const React = require('react');
8
+ const ReactDOM = require('react-dom/client');
9
+
10
+ const NS = 'dsh-cron';
11
+ const PANEL = 'cron';
12
+ const ACTIVATE_EVENT = 'dsh-panel-activate';
13
+ const ACTIVE_ATTR = 'data-dsh-cron-active';
14
+ const ENTRY_ATTR = 'data-dsh-cron-entry';
15
+ const VIEW_ATTR = 'data-dsh-cron-view';
16
+ const COLUMN_SELECTOR = '[data-pane=conversation], [class*=centerCol]';
17
+ const SESSION_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="newSession"]';
18
+
19
+ const ICON_TIMER = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
20
+ const ICON_PLAY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 21"></polygon></svg>';
21
+ const ICON_PAUSE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
22
+ const ICON_SEARCH = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
23
+ const ICON_CHEVRON_DOWN = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>';
24
+ const ICON_BELL = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
25
+ const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#a855f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
26
+ const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
27
+
28
+ const STYLES = `
29
+ html[${ACTIVE_ATTR}] [data-dsh-cron-view] { display: flex !important; }
30
+ html[${ACTIVE_ATTR}] [data-pane=conversation]>:not([data-dsh-cron-view]),
31
+ html[${ACTIVE_ATTR}] [class*=centerCol]>:not([data-dsh-cron-view]) { display: none !important; }
32
+
33
+ .dsh-cron-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-layer-2, #171717); color: var(--dsw-alias-label-primary, #ededed); overflow-y: auto; padding: 40px 48px; z-index: 100; }
34
+ .dsh-cron-container { max-width: 860px; width: 100%; margin: 0 auto; }
35
+ .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
36
+ .dsh-cron-title { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 6px; }
37
+ .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); }
38
+ .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-4, #262626); color: #fff; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
39
+ .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, #333); }
40
+ .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 200; }
41
+ .dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: #ededed; font-size: 13.5px; text-align: left; cursor: pointer; }
42
+ .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); }
43
+ .dsh-cron-search-bar { position: relative; margin-bottom: 20px; }
44
+ .dsh-cron-search-input { width: 100%; background: var(--dsw-alias-bg-layer-3, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 10px; padding: 10px 14px 10px 38px; color: inherit; font-size: 14px; outline: none; }
45
+ .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #666; }
46
+ .dsh-cron-tabs { display: flex; gap: 8px; margin-bottom: 24px; }
47
+ .dsh-cron-tab { appearance: none; background: transparent; border: none; padding: 6px 14px; border-radius: 9999px; color: #9ca3af; font-size: 13.5px; cursor: pointer; }
48
+ .dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: #fff; font-weight: 500; }
49
+ .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
50
+ .dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; }
51
+ .dsh-cron-task-item:hover { border-color: #444; }
52
+ .dsh-cron-task-status-btn { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: #9ca3af; border: 1px solid #333; cursor: pointer; flex-shrink: 0; }
53
+ .dsh-cron-task-status-btn[data-active="true"] { color: #10b981; border-color: rgba(16,185,129,0.3); }
54
+ .dsh-cron-task-info { flex: 1; min-width: 0; }
55
+ .dsh-cron-task-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
56
+ .dsh-cron-task-sched { font-size: 12.5px; color: #9ca3af; }
57
+ .dsh-cron-task-actions { display: flex; align-items: center; gap: 8px; }
58
+ .dsh-cron-icon-action { appearance: none; background: transparent; border: 1px solid transparent; border-radius: 8px; padding: 6px 10px; color: #9ca3af; font-size: 12px; cursor: pointer; }
59
+ .dsh-cron-icon-action:hover { background: #2a2a2a; color: #fff; }
60
+ .dsh-cron-rec-section { margin-top: 32px; }
61
+ .dsh-cron-rec-title { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
62
+ .dsh-cron-rec-list { display: flex; flex-direction: column; gap: 12px; }
63
+ .dsh-cron-rec-item { display: flex; align-items: flex-start; gap: 14px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid #2e2e2e; border-radius: 12px; cursor: pointer; }
64
+ .dsh-cron-rec-item:hover { border-color: #444; background: #232323; }
65
+ .dsh-cron-rec-item-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
66
+ .dsh-cron-rec-item-sched { font-size: 12.5px; color: #9ca3af; margin-left: 6px; font-weight: normal; }
67
+ .dsh-cron-rec-item-desc { font-size: 13px; color: #888; line-height: 1.4; }
68
+ .dsh-cron-modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 500; }
69
+ .dsh-cron-modal { width: 480px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid #383838; border-radius: 14px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
70
+ .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
71
+ .dsh-cron-field { margin-bottom: 14px; }
72
+ .dsh-cron-label { display: block; font-size: 12.5px; color: #aaa; margin-bottom: 6px; }
73
+ .dsh-cron-input, .dsh-cron-textarea { width: 100%; background: #2a2a2a; border: 1px solid #383838; border-radius: 8px; padding: 8px 12px; color: inherit; font-size: 13.5px; outline: none; }
74
+ .dsh-cron-textarea { height: 80px; resize: vertical; }
75
+ .dsh-cron-modal-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
76
+ .dsh-cron-btn-primary { background: #ededed; color: #111; border: none; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; }
77
+ .dsh-cron-btn-secondary { background: transparent; color: #999; border: 1px solid #383838; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; cursor: pointer; }
78
+ `;
79
+
80
+ if (typeof document !== 'undefined' && !document.getElementById('dsh-cron-styles')) {
81
+ const styleEl = document.createElement('style');
82
+ styleEl.id = 'dsh-cron-styles';
83
+ styleEl.textContent = STYLES;
84
+ document.head.appendChild(styleEl);
85
+ }
86
+
87
+ function createToggle() {
88
+ let open = false;
89
+ const listeners = new Set();
90
+ return {
91
+ isOpen: () => open,
92
+ set: (v) => { open = !!v; for (const fn of listeners) fn(open); },
93
+ toggle: () => { open = !open; for (const fn of listeners) fn(open); },
94
+ subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); }
95
+ };
96
+ }
97
+
98
+ function CronScreen(props) {
99
+ const { toggle } = props;
100
+ const [tab, setTab] = React.useState('all');
101
+ const [query, setQuery] = React.useState('');
102
+ const [tasks, setTasks] = React.useState([]);
103
+ const [recs, setRecs] = React.useState([]);
104
+ const [dropdownOpen, setDropdownOpen] = React.useState(false);
105
+ const [modalOpen, setModalOpen] = React.useState(false);
106
+ const [formTitle, setFormTitle] = React.useState('');
107
+ const [formSchedule, setFormSchedule] = React.useState('');
108
+ const [formPrompt, setFormPrompt] = React.useState('');
109
+
110
+ const fetchTasks = async () => {
111
+ try {
112
+ const res = await fetch('/dsh-cron/tasks?status=' + tab + '&query=' + encodeURIComponent(query));
113
+ const data = await res.json();
114
+ if (data && data.ok) {
115
+ setTasks(data.tasks || []);
116
+ setRecs(data.recommendations || []);
117
+ }
118
+ } catch (err) {}
119
+ };
120
+
121
+ React.useEffect(() => { fetchTasks(); }, [tab, query]);
122
+
123
+ const handleToggleTask = async (id) => {
124
+ try {
125
+ await fetch('/dsh-cron/action/' + id + '/toggle', { method: 'POST' });
126
+ fetchTasks();
127
+ } catch (err) {}
128
+ };
129
+
130
+ const handleRunNow = async (id) => {
131
+ try {
132
+ await fetch('/dsh-cron/action/' + id + '/run', { method: 'POST' });
133
+ alert('Задача запущена');
134
+ fetchTasks();
135
+ } catch (err) {}
136
+ };
137
+
138
+ const handleDelete = async (id) => {
139
+ if (!confirm('Удалить эту задачу?')) return;
140
+ try {
141
+ await fetch('/dsh-cron/action/' + id + '/delete', { method: 'DELETE' });
142
+ fetchTasks();
143
+ } catch (err) {}
144
+ };
145
+
146
+ const openCreateModal = (preset) => {
147
+ setDropdownOpen(false);
148
+ if (preset) {
149
+ setFormTitle(preset.title || '');
150
+ setFormSchedule(preset.schedule || '');
151
+ setFormPrompt(preset.prompt || '');
152
+ } else {
153
+ setFormTitle('');
154
+ setFormSchedule('0 9 * * 1-5');
155
+ setFormPrompt('');
156
+ }
157
+ setModalOpen(true);
158
+ };
159
+
160
+ const handleSave = async (e) => {
161
+ e.preventDefault();
162
+ try {
163
+ const payload = { title: formTitle, schedule: formSchedule, prompt: formPrompt };
164
+ const res = await fetch('/dsh-cron/tasks', {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify(payload)
168
+ });
169
+ const data = await res.json();
170
+ if (!data.ok) { alert(data.error || 'Ошибка'); return; }
171
+ setModalOpen(false);
172
+ fetchTasks();
173
+ } catch (err) { alert(err.message); }
174
+ };
175
+
176
+ const handleCreateWithDSH = () => {
177
+ setDropdownOpen(false);
178
+ toggle.set(false);
179
+ setTimeout(() => {
180
+ const textarea = document.querySelector('textarea[placeholder*="message"], textarea[class*="prompt"]');
181
+ if (textarea) {
182
+ textarea.value = 'Помоги мне настроить новую запланированную задачу cron: ';
183
+ textarea.focus();
184
+ }
185
+ }, 150);
186
+ };
187
+
188
+ return React.createElement('div', { className: 'dsh-cron-overlay' },
189
+ React.createElement('div', { className: 'dsh-cron-container' },
190
+ React.createElement('div', { className: 'dsh-cron-header' },
191
+ React.createElement('div', null,
192
+ React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
193
+ React.createElement('div', { className: 'dsh-cron-subtitle' }, 'Попросите DSH планировать задачи, ставить напоминания или отслеживать обновления')
194
+ ),
195
+ React.createElement('div', { style: { position: 'relative' } },
196
+ React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
197
+ 'Создать',
198
+ React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
199
+ ),
200
+ dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
201
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: handleCreateWithDSH }, '💬 Создать с DSH'),
202
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openCreateModal(null) }, '✏️ Настроить вручную')
203
+ )
204
+ )
205
+ ),
206
+ React.createElement('div', { className: 'dsh-cron-search-bar' },
207
+ React.createElement('span', { className: 'dsh-cron-search-icon', dangerouslySetInnerHTML: { __html: ICON_SEARCH } }),
208
+ React.createElement('input', {
209
+ className: 'dsh-cron-search-input',
210
+ placeholder: 'Поиск запланированных задач',
211
+ value: query,
212
+ onChange: (e) => setQuery(e.target.value)
213
+ })
214
+ ),
215
+ React.createElement('div', { className: 'dsh-cron-tabs' },
216
+ ['all', 'active', 'paused', 'completed'].map((t) =>
217
+ React.createElement('button', {
218
+ key: t,
219
+ className: 'dsh-cron-tab',
220
+ 'data-active': tab === t ? 'true' : undefined,
221
+ onClick: () => setTab(t)
222
+ },
223
+ t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'Приостановленные' : 'Завершено'
224
+ )
225
+ )
226
+ ),
227
+ React.createElement('div', { className: 'dsh-cron-task-list' },
228
+ tasks.length === 0 ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: '#666' } }, 'Нет задач в выбранном статусе') :
229
+ tasks.map((task) =>
230
+ React.createElement('div', { key: task.id, className: 'dsh-cron-task-item' },
231
+ React.createElement('button', {
232
+ className: 'dsh-cron-task-status-btn',
233
+ 'data-active': task.status === 'active' ? 'true' : undefined,
234
+ onClick: () => handleToggleTask(task.id),
235
+ dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PLAY : ICON_PAUSE }
236
+ }),
237
+ React.createElement('div', { className: 'dsh-cron-task-info' },
238
+ React.createElement('div', { className: 'dsh-cron-task-title' }, task.title),
239
+ React.createElement('div', { className: 'dsh-cron-task-sched' }, task.scheduleText || task.schedule)
240
+ ),
241
+ React.createElement('div', { className: 'dsh-cron-task-actions' },
242
+ React.createElement('button', { className: 'dsh-cron-icon-action', onClick: () => handleRunNow(task.id) }, 'Запустить сейчас'),
243
+ React.createElement('button', { className: 'dsh-cron-icon-action', onClick: () => handleDelete(task.id) }, 'Удалить')
244
+ )
245
+ )
246
+ )
247
+ ),
248
+ React.createElement('div', { className: 'dsh-cron-rec-section' },
249
+ React.createElement('div', { className: 'dsh-cron-rec-title' }, 'Рекомендации'),
250
+ React.createElement('div', { className: 'dsh-cron-rec-list' },
251
+ recs.map((rec) =>
252
+ React.createElement('div', { key: rec.id, className: 'dsh-cron-rec-item', onClick: () => openCreateModal(rec) },
253
+ React.createElement('div', {
254
+ className: 'dsh-cron-rec-icon',
255
+ dangerouslySetInnerHTML: { __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY }
256
+ }),
257
+ React.createElement('div', null,
258
+ React.createElement('div', { className: 'dsh-cron-rec-item-title' }, rec.title, React.createElement('span', { className: 'dsh-cron-rec-item-sched' }, rec.scheduleText)),
259
+ React.createElement('div', { className: 'dsh-cron-rec-item-desc' }, rec.description)
260
+ )
261
+ )
262
+ )
263
+ )
264
+ )
265
+ ),
266
+ modalOpen && React.createElement('div', { className: 'dsh-cron-modal-backdrop', onClick: () => setModalOpen(false) },
267
+ React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
268
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Настройка задачи вручную'),
269
+ React.createElement('form', { onSubmit: handleSave },
270
+ React.createElement('div', { className: 'dsh-cron-field' },
271
+ React.createElement('label', { className: 'dsh-cron-label' }, 'Название задачи'),
272
+ React.createElement('input', { className: 'dsh-cron-input', required: true, value: formTitle, onChange: (e) => setFormTitle(e.target.value) })
273
+ ),
274
+ React.createElement('div', { className: 'dsh-cron-field' },
275
+ React.createElement('label', { className: 'dsh-cron-label' }, 'Расписание (cron или interval)'),
276
+ React.createElement('input', { className: 'dsh-cron-input', required: true, value: formSchedule, onChange: (e) => setFormSchedule(e.target.value) })
277
+ ),
278
+ React.createElement('div', { className: 'dsh-cron-field' },
279
+ React.createElement('label', { className: 'dsh-cron-label' }, 'Инструкция для агента (промпт)'),
280
+ React.createElement('textarea', { className: 'dsh-cron-textarea', required: true, value: formPrompt, onChange: (e) => setFormPrompt(e.target.value) })
281
+ ),
282
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
283
+ React.createElement('button', { type: 'button', className: 'dsh-cron-btn-secondary', onClick: () => setModalOpen(false) }, 'Отмена'),
284
+ React.createElement('button', { type: 'submit', className: 'dsh-cron-btn-primary' }, 'Сохранить')
285
+ )
286
+ )
287
+ )
288
+ )
289
+ );
290
+ }
291
+
292
+ function nativeButton() {
293
+ const nodes = document.querySelectorAll('button[class*="newSession"]');
294
+ for (const node of nodes) {
295
+ if (String(node.className).indexOf('newSessionLabel') === -1) return node;
296
+ }
297
+ return undefined;
298
+ }
299
+
300
+ function createEntry(target, toggle, label) {
301
+ const entry = target.cloneNode(true);
302
+ entry.className = String(target.className) + ' dsh-cron-entry-clone';
303
+ entry.setAttribute(ENTRY_ATTR, '');
304
+ entry.setAttribute('data-dsh-plugin', 'cron');
305
+ entry.setAttribute('data-dsh-part', 'sidebar-entry');
306
+ entry.setAttribute('aria-label', label);
307
+ entry.removeAttribute('id');
308
+ entry.innerHTML = '';
309
+
310
+ const icon = document.createElement('span');
311
+ icon.className = 'dsh-cron-entry-icon';
312
+ icon.style.marginRight = '8px';
313
+ icon.innerHTML = ICON_TIMER;
314
+ entry.appendChild(icon);
315
+
316
+ const text = document.createElement('span');
317
+ text.className = 'dsh-cron-entry-label';
318
+ text.textContent = label;
319
+ entry.appendChild(text);
320
+
321
+ entry.addEventListener('click', (event) => {
322
+ event.preventDefault();
323
+ event.stopPropagation();
324
+ toggle.toggle();
325
+ });
326
+ return entry;
327
+ }
328
+
329
+ function placeEntry(target, entry) {
330
+ let anchor = target;
331
+ let next = target.nextElementSibling;
332
+ while (next !== null && next.tagName === 'BUTTON' && String(next.className).indexOf('sidebar-entry') >= 0) {
333
+ anchor = next;
334
+ next = next.nextElementSibling;
335
+ }
336
+ anchor.insertAdjacentElement('afterend', entry);
337
+ return true;
338
+ }
339
+
340
+ function mountSidebarEntry(toggle, label) {
341
+ if (typeof document === 'undefined') return () => {};
342
+ let entry;
343
+ const tryPlace = () => {
344
+ if (entry !== undefined && entry.isConnected) return;
345
+ const target = nativeButton();
346
+ if (target === undefined || target.parentElement === null) return;
347
+ if (target.parentElement.querySelector('[' + ENTRY_ATTR + ']') !== null) return;
348
+ entry = createEntry(target, toggle, label);
349
+ placeEntry(target, entry);
350
+ syncActive();
351
+ };
352
+
353
+ const syncActive = () => {
354
+ if (entry === undefined) return;
355
+ if (toggle.isOpen()) entry.dataset.active = 'true';
356
+ else delete entry.dataset.active;
357
+ };
358
+
359
+ const observer = new MutationObserver(tryPlace);
360
+ observer.observe(document.body, { childList: true, subtree: true });
361
+ const unsubscribe = toggle.subscribe(syncActive);
362
+ tryPlace();
363
+
364
+ return () => {
365
+ observer.disconnect();
366
+ unsubscribe();
367
+ if (entry !== undefined) entry.remove();
368
+ entry = undefined;
369
+ };
370
+ }
371
+
372
+ function mountCronScreen(ctx, toggle) {
373
+ if (typeof document === 'undefined') return () => {};
374
+ let root;
375
+ let container;
376
+
377
+ const ensure = () => {
378
+ if (container !== undefined) return;
379
+ const column = document.querySelector(COLUMN_SELECTOR);
380
+ if (column === null) return;
381
+ container = document.createElement('div');
382
+ container.setAttribute(VIEW_ATTR, '');
383
+ container.setAttribute('data-dsh-plugin', 'cron');
384
+ column.appendChild(container);
385
+ root = ReactDOM.createRoot(container);
386
+ root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }));
387
+ };
388
+
389
+ const applyActive = () => {
390
+ if (toggle.isOpen()) {
391
+ document.documentElement.setAttribute(ACTIVE_ATTR, '');
392
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL }));
393
+ } else {
394
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
395
+ }
396
+ };
397
+
398
+ const onOtherActivate = (event) => {
399
+ if (event.detail !== PANEL && toggle.isOpen()) toggle.set(false);
400
+ };
401
+ const onSidebarClick = (event) => {
402
+ if (!toggle.isOpen()) return;
403
+ const target = event.target;
404
+ if (target !== null && target.closest && target.closest(SESSION_ROW_SELECTOR) !== null) toggle.set(false);
405
+ };
406
+
407
+ const waitObserver = new MutationObserver(ensure);
408
+ waitObserver.observe(document.body, { childList: true, subtree: true });
409
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
410
+ document.addEventListener('click', onSidebarClick, true);
411
+ const unsubscribe = toggle.subscribe(applyActive);
412
+ applyActive();
413
+ ensure();
414
+
415
+ return () => {
416
+ waitObserver.disconnect();
417
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
418
+ document.removeEventListener('click', onSidebarClick, true);
419
+ unsubscribe();
420
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
421
+ if (root !== undefined) root.unmount();
422
+ root = undefined;
423
+ if (container !== undefined) container.remove();
424
+ container = undefined;
425
+ };
426
+ }
427
+
428
+ function apply(ctx) {
429
+ const toggle = createToggle();
430
+ ctx.effect(() => {
431
+ const off = [
432
+ mountSidebarEntry(toggle, 'Запланированные задачи'),
433
+ mountCronScreen(ctx, toggle)
434
+ ];
435
+ return () => { for (const dispose of off) dispose(); };
436
+ }, 'dsh-cron: client overlay');
437
+ }
438
+
439
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
440
+ return module.exports;
441
+ }
442
+ });