@yemi33/minions 0.1.11 → 0.1.13
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/CHANGELOG.md +60 -0
- package/dashboard/js/command-center.js +377 -0
- package/dashboard/js/command-history.js +70 -0
- package/dashboard/js/command-input.js +268 -0
- package/dashboard/js/command-parser.js +129 -0
- package/dashboard/js/detail-panel.js +98 -0
- package/dashboard/js/live-stream.js +69 -0
- package/dashboard/js/modal-qa.js +268 -0
- package/dashboard/js/modal.js +131 -0
- package/dashboard/js/refresh.js +59 -0
- package/dashboard/js/render-agents.js +17 -0
- package/dashboard/js/render-dispatch.js +148 -0
- package/dashboard/js/render-inbox.js +126 -0
- package/dashboard/js/render-kb.js +107 -0
- package/dashboard/js/render-other.js +181 -0
- package/dashboard/js/render-plans.js +304 -0
- package/dashboard/js/render-prd.js +469 -0
- package/dashboard/js/render-prs.js +94 -0
- package/dashboard/js/render-schedules.js +158 -0
- package/dashboard/js/render-skills.js +89 -0
- package/dashboard/js/render-work-items.js +219 -0
- package/dashboard/js/settings.js +135 -0
- package/dashboard/js/state.js +84 -0
- package/dashboard/js/utils.js +39 -0
- package/dashboard/layout.html +123 -0
- package/dashboard/pages/engine.html +12 -0
- package/dashboard/pages/home.html +31 -0
- package/dashboard/pages/inbox.html +17 -0
- package/dashboard/pages/plans.html +4 -0
- package/dashboard/pages/prd.html +5 -0
- package/dashboard/pages/prs.html +4 -0
- package/dashboard/pages/schedule.html +10 -0
- package/dashboard/pages/work.html +5 -0
- package/dashboard/styles.css +598 -0
- package/dashboard-build.js +51 -0
- package/dashboard.html +179 -107
- package/dashboard.js +51 -1
- package/engine/ado.js +14 -0
- package/engine/cli.js +11 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +25 -29
- package/engine.js +106 -19
- package/package.json +1 -1
- package/routing.md +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// render-schedules.js — Schedule rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderSchedules(schedules) {
|
|
4
|
+
const el = document.getElementById('scheduled-content');
|
|
5
|
+
const countEl = document.getElementById('scheduled-count');
|
|
6
|
+
countEl.textContent = schedules.length;
|
|
7
|
+
if (!schedules.length) {
|
|
8
|
+
el.innerHTML = '<p class="empty">No scheduled tasks. Add one to automate recurring work.</p>';
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Cron</th><th>Type</th><th>Project</th><th>Agent</th><th>Enabled</th><th>Last Run</th><th></th></tr></thead><tbody>';
|
|
12
|
+
for (const s of schedules) {
|
|
13
|
+
const enabledBadge = s.enabled
|
|
14
|
+
? '<span class="pr-badge approved">enabled</span>'
|
|
15
|
+
: '<span class="pr-badge rejected">disabled</span>';
|
|
16
|
+
const lastRun = s._lastRun ? timeAgo(s._lastRun) : 'never';
|
|
17
|
+
const typeBadge = '<span class="dispatch-type ' + escHtml(s.type || 'implement') + '">' + escHtml(s.type || 'implement') + '</span>';
|
|
18
|
+
html += '<tr>' +
|
|
19
|
+
'<td><span class="pr-id">' + escHtml(s.id || '') + '</span></td>' +
|
|
20
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(s.title || '') + '">' + escHtml(s.title || '') + '</td>' +
|
|
21
|
+
'<td><code style="font-size:10px;color:var(--blue)">' + escHtml(s.cron || '') + '</code></td>' +
|
|
22
|
+
'<td>' + typeBadge + '</td>' +
|
|
23
|
+
'<td><span style="font-size:10px;color:var(--muted)">' + escHtml(s.project || '') + '</span></td>' +
|
|
24
|
+
'<td><span class="pr-agent">' + escHtml(s.agent || 'auto') + '</span></td>' +
|
|
25
|
+
'<td>' + enabledBadge + '</td>' +
|
|
26
|
+
'<td><span class="pr-date">' + escHtml(lastRun) + '</span></td>' +
|
|
27
|
+
'<td style="white-space:nowrap">' +
|
|
28
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';border-color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';margin-right:4px" onclick="event.stopPropagation();toggleScheduleEnabled(\'' + escHtml(s.id) + '\',' + !s.enabled + ')" title="' + (s.enabled ? 'Disable' : 'Enable') + '">' + (s.enabled ? '⏸' : '▶') + '</button>' +
|
|
29
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();openEditScheduleModal(\'' + escHtml(s.id) + '\')" title="Edit">✎</button>' +
|
|
30
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteSchedule(\'' + escHtml(s.id) + '\')" title="Delete">✕</button>' +
|
|
31
|
+
'</td>' +
|
|
32
|
+
'</tr>';
|
|
33
|
+
}
|
|
34
|
+
html += '</tbody></table></div>';
|
|
35
|
+
el.innerHTML = html;
|
|
36
|
+
window._lastSchedules = schedules;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _scheduleFormHtml(sched, isEdit) {
|
|
40
|
+
const types = ['implement', 'test', 'explore', 'ask', 'review', 'fix'];
|
|
41
|
+
const priorities = ['high', 'medium', 'low'];
|
|
42
|
+
const typeOpts = types.map(t => '<option value="' + t + '"' + ((sched.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
43
|
+
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((sched.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
44
|
+
const projOpts = '<option value="">Any</option>' + cmdProjects.map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.name) + '</option>').join('');
|
|
45
|
+
const agentOpts = '<option value="">Auto</option>' + cmdAgents.map(a => '<option value="' + escHtml(a.id) + '"' + (sched.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
46
|
+
|
|
47
|
+
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
48
|
+
|
|
49
|
+
return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
50
|
+
(isEdit ? '' :
|
|
51
|
+
'<label style="color:var(--text);font-size:var(--text-md)">ID (unique slug)' +
|
|
52
|
+
'<input id="sched-edit-id" value="' + escHtml(sched.id || '') + '" placeholder="e.g. nightly-tests" style="' + inputStyle + '">' +
|
|
53
|
+
'</label>') +
|
|
54
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Title' +
|
|
55
|
+
'<input id="sched-edit-title" value="' + escHtml(sched.title || '') + '" style="' + inputStyle + '">' +
|
|
56
|
+
'</label>' +
|
|
57
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Cron <span style="font-size:10px;color:var(--muted)">(minute hour dayOfWeek)</span>' +
|
|
58
|
+
'<input id="sched-edit-cron" value="' + escHtml(sched.cron || '') + '" placeholder="0 2 *" style="' + inputStyle + '">' +
|
|
59
|
+
'</label>' +
|
|
60
|
+
'<div style="display:flex;gap:12px">' +
|
|
61
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Type' +
|
|
62
|
+
'<select id="sched-edit-type" style="' + inputStyle + '">' + typeOpts + '</select>' +
|
|
63
|
+
'</label>' +
|
|
64
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Priority' +
|
|
65
|
+
'<select id="sched-edit-priority" style="' + inputStyle + '">' + priOpts + '</select>' +
|
|
66
|
+
'</label>' +
|
|
67
|
+
'</div>' +
|
|
68
|
+
'<div style="display:flex;gap:12px">' +
|
|
69
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Project' +
|
|
70
|
+
'<select id="sched-edit-project" style="' + inputStyle + '">' + projOpts + '</select>' +
|
|
71
|
+
'</label>' +
|
|
72
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Agent' +
|
|
73
|
+
'<select id="sched-edit-agent" style="' + inputStyle + '">' + agentOpts + '</select>' +
|
|
74
|
+
'</label>' +
|
|
75
|
+
'</div>' +
|
|
76
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Description' +
|
|
77
|
+
'<textarea id="sched-edit-desc" rows="3" style="' + inputStyle + ';resize:vertical">' + escHtml(sched.description || '') + '</textarea>' +
|
|
78
|
+
'</label>' +
|
|
79
|
+
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
|
|
80
|
+
'<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
|
|
81
|
+
'<button onclick="submitSchedule(' + isEdit + ')" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">' + (isEdit ? 'Save' : 'Create') + '</button>' +
|
|
82
|
+
'</div>' +
|
|
83
|
+
'</div>';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openCreateScheduleModal() {
|
|
87
|
+
document.getElementById('modal-title').textContent = 'New Scheduled Task';
|
|
88
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
89
|
+
document.getElementById('modal-body').style.fontFamily = '';
|
|
90
|
+
document.getElementById('modal-body').innerHTML = _scheduleFormHtml({}, false);
|
|
91
|
+
document.getElementById('modal').classList.add('open');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function openEditScheduleModal(id) {
|
|
95
|
+
const sched = (window._lastSchedules || []).find(s => s.id === id);
|
|
96
|
+
if (!sched) return;
|
|
97
|
+
document.getElementById('modal-title').textContent = 'Edit Schedule: ' + id;
|
|
98
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
99
|
+
document.getElementById('modal-body').style.fontFamily = '';
|
|
100
|
+
document.getElementById('modal-body').innerHTML = _scheduleFormHtml(sched, true);
|
|
101
|
+
window._editScheduleId = id;
|
|
102
|
+
document.getElementById('modal').classList.add('open');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function submitSchedule(isEdit) {
|
|
106
|
+
const title = document.getElementById('sched-edit-title').value.trim();
|
|
107
|
+
const cron = document.getElementById('sched-edit-cron').value.trim();
|
|
108
|
+
const type = document.getElementById('sched-edit-type').value;
|
|
109
|
+
const priority = document.getElementById('sched-edit-priority').value;
|
|
110
|
+
const project = document.getElementById('sched-edit-project').value;
|
|
111
|
+
const agent = document.getElementById('sched-edit-agent').value;
|
|
112
|
+
const description = document.getElementById('sched-edit-desc').value;
|
|
113
|
+
const id = isEdit ? window._editScheduleId : (document.getElementById('sched-edit-id') ? document.getElementById('sched-edit-id').value.trim() : '');
|
|
114
|
+
|
|
115
|
+
if (!id) { alert('ID is required'); return; }
|
|
116
|
+
if (!title) { alert('Title is required'); return; }
|
|
117
|
+
if (!cron) { alert('Cron expression is required'); return; }
|
|
118
|
+
|
|
119
|
+
const payload = { id, title, cron, type, priority, project: project || undefined, agent: agent || undefined, description: description || undefined, enabled: true };
|
|
120
|
+
const url = isEdit ? '/api/schedules/update' : '/api/schedules';
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(url, {
|
|
123
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify(payload)
|
|
125
|
+
});
|
|
126
|
+
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', isEdit ? 'Schedule updated' : 'Schedule created', true); } else {
|
|
127
|
+
const d = await res.json().catch(() => ({}));
|
|
128
|
+
alert((isEdit ? 'Update' : 'Create') + ' failed: ' + (d.error || 'unknown'));
|
|
129
|
+
}
|
|
130
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function toggleScheduleEnabled(id, enabled) {
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch('/api/schedules/update', {
|
|
136
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ id, enabled })
|
|
138
|
+
});
|
|
139
|
+
if (res.ok) { refresh(); } else {
|
|
140
|
+
const d = await res.json().catch(() => ({}));
|
|
141
|
+
alert('Toggle failed: ' + (d.error || 'unknown'));
|
|
142
|
+
}
|
|
143
|
+
} catch (e) { alert('Toggle error: ' + e.message); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function deleteSchedule(id) {
|
|
147
|
+
if (!confirm('Delete scheduled task "' + id + '"?')) return;
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch('/api/schedules/delete', {
|
|
150
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ id })
|
|
152
|
+
});
|
|
153
|
+
if (res.ok) { refresh(); showToast('cmd-toast', 'Schedule deleted', true); } else {
|
|
154
|
+
const d = await res.json().catch(() => ({}));
|
|
155
|
+
alert('Delete failed: ' + (d.error || 'unknown'));
|
|
156
|
+
}
|
|
157
|
+
} catch (e) { alert('Delete error: ' + e.message); }
|
|
158
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// render-skills.js — Skills rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
let _skillsTab = 'all';
|
|
4
|
+
let _skillsPage = 0;
|
|
5
|
+
const SKILLS_PER_PAGE = 5;
|
|
6
|
+
|
|
7
|
+
function renderSkills(skills) {
|
|
8
|
+
const el = document.getElementById('skills-list');
|
|
9
|
+
const countEl = document.getElementById('skills-count');
|
|
10
|
+
countEl.textContent = skills.length;
|
|
11
|
+
if (!skills.length) {
|
|
12
|
+
el.innerHTML = '<p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p>';
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sourceIcon = (s) => s === 'claude-code' ? '⚡' : s === 'plugin' ? '🔌' : s?.startsWith('project:') ? '📁' : '🔧';
|
|
17
|
+
const sourceLabel = (s) => s === 'plugin' ? 'plugin' : s?.startsWith('project:') ? s.replace('project:', '') : 'global';
|
|
18
|
+
|
|
19
|
+
// Group by source
|
|
20
|
+
const groups = {};
|
|
21
|
+
for (const r of skills) {
|
|
22
|
+
const key = r.source === 'plugin' ? 'plugins' : r.source?.startsWith('project:') ? r.source.replace('project:', '') : 'global';
|
|
23
|
+
if (!groups[key]) groups[key] = [];
|
|
24
|
+
groups[key].push(r);
|
|
25
|
+
}
|
|
26
|
+
const groupKeys = Object.keys(groups).sort((a, b) => a === 'global' ? -1 : b === 'global' ? 1 : a.localeCompare(b));
|
|
27
|
+
|
|
28
|
+
// Tab bar
|
|
29
|
+
const tabs = [{ key: 'all', label: 'All (' + skills.length + ')' }];
|
|
30
|
+
for (const k of groupKeys) tabs.push({ key: k, label: k + ' (' + groups[k].length + ')' });
|
|
31
|
+
|
|
32
|
+
let html = '<div style="display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap">';
|
|
33
|
+
for (const t of tabs) {
|
|
34
|
+
const active = _skillsTab === t.key;
|
|
35
|
+
html += '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' +
|
|
36
|
+
(active ? 'background:var(--green);color:#fff;border-color:var(--green)' : '') +
|
|
37
|
+
'" onclick="_skillsTab=\'' + escHtml(t.key) + '\';_skillsPage=0;renderSkills(window._lastSkills)">' + escHtml(t.label) + '</button>';
|
|
38
|
+
}
|
|
39
|
+
html += '</div>';
|
|
40
|
+
|
|
41
|
+
// Filter by tab
|
|
42
|
+
const filtered = _skillsTab === 'all' ? skills : (groups[_skillsTab] || []);
|
|
43
|
+
|
|
44
|
+
// Paginate
|
|
45
|
+
const totalPages = Math.ceil(filtered.length / SKILLS_PER_PAGE);
|
|
46
|
+
if (_skillsPage >= totalPages) _skillsPage = Math.max(0, totalPages - 1);
|
|
47
|
+
const page = filtered.slice(_skillsPage * SKILLS_PER_PAGE, (_skillsPage + 1) * SKILLS_PER_PAGE);
|
|
48
|
+
|
|
49
|
+
html += '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
50
|
+
for (const r of page) {
|
|
51
|
+
const autoTag = r.autoGenerated ? '<span style="font-size:8px;background:rgba(63,185,80,0.15);color:var(--green);padding:1px 4px;border-radius:3px;margin-left:4px">auto</span>' : '';
|
|
52
|
+
html += '<div class="inbox-item" onclick="openSkill(\'' + escHtml(r.file) + '\',\'' + escHtml(r.source || 'claude-code') + '\',\'' + escHtml(r.dir || '') + '\')" style="border-left-color:var(--green)">' +
|
|
53
|
+
'<div class="inbox-name"><span style="color:var(--green);font-weight:600">' + sourceIcon(r.source) + ' ' + escHtml(r.name) + '</span>' + autoTag +
|
|
54
|
+
'<span style="font-size:9px;color:var(--muted);margin-left:auto">' + escHtml(sourceLabel(r.source)) + '</span>' +
|
|
55
|
+
'</div>' +
|
|
56
|
+
(r.description ? '<div class="inbox-preview" style="color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
|
|
57
|
+
'</div>';
|
|
58
|
+
}
|
|
59
|
+
html += '</div>';
|
|
60
|
+
|
|
61
|
+
// Pagination controls
|
|
62
|
+
if (totalPages > 1) {
|
|
63
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-top:6px;justify-content:center">';
|
|
64
|
+
html += '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" ' +
|
|
65
|
+
(_skillsPage === 0 ? 'disabled' : '') +
|
|
66
|
+
' onclick="_skillsPage--;renderSkills(window._lastSkills)">«</button>';
|
|
67
|
+
html += '<span style="font-size:10px;color:var(--muted)">' + (_skillsPage + 1) + ' / ' + totalPages + '</span>';
|
|
68
|
+
html += '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" ' +
|
|
69
|
+
(_skillsPage >= totalPages - 1 ? 'disabled' : '') +
|
|
70
|
+
' onclick="_skillsPage++;renderSkills(window._lastSkills)">»</button>';
|
|
71
|
+
html += '</div>';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
el.innerHTML = html;
|
|
75
|
+
window._lastSkills = skills;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function openSkill(file, source, dir) {
|
|
79
|
+
fetch('/api/skill?file=' + encodeURIComponent(file) + '&source=' + encodeURIComponent(source || 'claude-code') + (dir ? '&dir=' + encodeURIComponent(dir) : ''))
|
|
80
|
+
.then(r => r.text())
|
|
81
|
+
.then(content => {
|
|
82
|
+
document.getElementById('modal-title').textContent = file;
|
|
83
|
+
document.getElementById('modal-body').textContent = content;
|
|
84
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
85
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
86
|
+
document.getElementById('modal').classList.add('open');
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// dashboard/js/render-work-items.js — Work item rendering and management extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
let allWorkItems = [];
|
|
4
|
+
let wiPage = 0;
|
|
5
|
+
const WI_PER_PAGE = 6;
|
|
6
|
+
|
|
7
|
+
function wiRow(item) {
|
|
8
|
+
const statusBadge = (s) => {
|
|
9
|
+
const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
10
|
+
return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
|
|
11
|
+
};
|
|
12
|
+
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
|
|
13
|
+
const priBadge = (p) => '<span class="prd-item-priority ' + (p || '') + '">' + escHtml(p || 'medium') + '</span>';
|
|
14
|
+
const prLink = item._pr
|
|
15
|
+
? '<a class="pr-title" href="' + escHtml(item._prUrl || '#') + '" target="_blank" style="font-size:10px">' + escHtml(item._pr) + '</a>'
|
|
16
|
+
: '<span style="color:var(--muted)">—</span>';
|
|
17
|
+
return '<tr>' +
|
|
18
|
+
'<td><span class="pr-id">' + escHtml(item.id || '') + '</span></td>' +
|
|
19
|
+
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(item.description || item.title || '') + '">' + escHtml(item.title || '') + '</td>' +
|
|
20
|
+
'<td><span style="font-size:10px;color:var(--muted)">' + escHtml(item._source || '') + '</span>' +
|
|
21
|
+
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:8px">fan-out</span>' : '') + '</td>' +
|
|
22
|
+
'<td>' + typeBadge(item.type) + '</td>' +
|
|
23
|
+
'<td>' + priBadge(item.priority) + '</td>' +
|
|
24
|
+
'<td>' + statusBadge(item.status || 'pending') +
|
|
25
|
+
(item._pendingReason ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' + escHtml(item._pendingReason) + '">' + escHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
26
|
+
(item.status === 'failed' ? ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry</button>' : '') +
|
|
27
|
+
'</td>' +
|
|
28
|
+
'<td>' +
|
|
29
|
+
(item.completedAgents && item.completedAgents.length > 0
|
|
30
|
+
? '<span class="pr-agent">' + escHtml(item.completedAgents.join(', ')) + '</span>'
|
|
31
|
+
: '<span class="pr-agent">' + escHtml(item.dispatched_to || '—') + '</span>') +
|
|
32
|
+
(item.failReason ? '<span style="display:block;font-size:9px;color:var(--red)" title="' + escHtml(item.failReason) + '">' + escHtml(item.failReason.slice(0, 30)) + '</span>' : '') +
|
|
33
|
+
'</td>' +
|
|
34
|
+
'<td>' + prLink + '</td>' +
|
|
35
|
+
'<td><span class="pr-date">' + shortTime(item.created) + '</span></td>' +
|
|
36
|
+
'<td style="white-space:nowrap">' +
|
|
37
|
+
((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">✎</button>' : '') +
|
|
38
|
+
((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
39
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
40
|
+
'</td>' +
|
|
41
|
+
'</tr>';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderWorkItems(items) {
|
|
45
|
+
// Sort: active/dispatched first, then by most recent activity
|
|
46
|
+
const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
|
|
47
|
+
items.sort((a, b) => {
|
|
48
|
+
const sa = statusOrder[a.status] ?? 2, sb = statusOrder[b.status] ?? 2;
|
|
49
|
+
if (sa !== sb) return sa - sb;
|
|
50
|
+
const ta = a.completedAt || a.dispatched_at || a.created || '';
|
|
51
|
+
const tb = b.completedAt || b.dispatched_at || b.created || '';
|
|
52
|
+
return tb.localeCompare(ta); // most recent first
|
|
53
|
+
});
|
|
54
|
+
allWorkItems = items;
|
|
55
|
+
const el = document.getElementById('work-items-content');
|
|
56
|
+
const countEl = document.getElementById('wi-count');
|
|
57
|
+
countEl.textContent = items.length;
|
|
58
|
+
if (!items.length) {
|
|
59
|
+
el.innerHTML = '<p class="empty">No work items. Add tasks via Command Center above.</p>';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const totalPages = Math.ceil(items.length / WI_PER_PAGE);
|
|
64
|
+
if (wiPage >= totalPages) wiPage = totalPages - 1;
|
|
65
|
+
const start = wiPage * WI_PER_PAGE;
|
|
66
|
+
const pageItems = items.slice(start, start + WI_PER_PAGE);
|
|
67
|
+
|
|
68
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>';
|
|
69
|
+
html += pageItems.map(wiRow).join('');
|
|
70
|
+
html += '</tbody></table></div>';
|
|
71
|
+
|
|
72
|
+
if (items.length > WI_PER_PAGE) {
|
|
73
|
+
html += '<div class="pr-pager">' +
|
|
74
|
+
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+WI_PER_PAGE, items.length) + ' of ' + items.length + '</span>' +
|
|
75
|
+
'<div class="pr-pager-btns">' +
|
|
76
|
+
'<button class="pr-pager-btn ' + (wiPage === 0 ? 'disabled' : '') + '" onclick="wiPrev()">Prev</button>' +
|
|
77
|
+
'<button class="pr-pager-btn ' + (wiPage >= totalPages-1 ? 'disabled' : '') + '" onclick="wiNext()">Next</button>' +
|
|
78
|
+
'<button class="pr-pager-btn see-all" onclick="openAllWorkItems()">See all ' + items.length + '</button>' +
|
|
79
|
+
'</div>' +
|
|
80
|
+
'</div>';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
el.innerHTML = html;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function editWorkItem(id, source) {
|
|
87
|
+
const item = allWorkItems.find(i => i.id === id);
|
|
88
|
+
if (!item) return;
|
|
89
|
+
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'investigate', 'refactor', 'test', 'docs'];
|
|
90
|
+
const priorities = ['critical', 'high', 'medium', 'low'];
|
|
91
|
+
const agentOpts = cmdAgents.map(a => '<option value="' + escHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
92
|
+
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
93
|
+
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((item.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
94
|
+
|
|
95
|
+
document.getElementById('modal-title').textContent = 'Edit Work Item ' + id;
|
|
96
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
97
|
+
document.getElementById('modal-body').innerHTML =
|
|
98
|
+
'<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
99
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Title' +
|
|
100
|
+
'<input id="wi-edit-title" value="' + escHtml(item.title || '') + '" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit">' +
|
|
101
|
+
'</label>' +
|
|
102
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Description' +
|
|
103
|
+
'<textarea id="wi-edit-desc" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escHtml(item.description || '') + '</textarea>' +
|
|
104
|
+
'</label>' +
|
|
105
|
+
'<div style="display:flex;gap:12px">' +
|
|
106
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Type' +
|
|
107
|
+
'<select id="wi-edit-type" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' + typeOpts + '</select>' +
|
|
108
|
+
'</label>' +
|
|
109
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Priority' +
|
|
110
|
+
'<select id="wi-edit-priority" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' + priOpts + '</select>' +
|
|
111
|
+
'</label>' +
|
|
112
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Agent' +
|
|
113
|
+
'<select id="wi-edit-agent" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)"><option value="">Auto</option>' + agentOpts + '</select>' +
|
|
114
|
+
'</label>' +
|
|
115
|
+
'</div>' +
|
|
116
|
+
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
|
|
117
|
+
'<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
|
|
118
|
+
'<button onclick="submitWorkItemEdit(\'' + escHtml(id) + '\',\'' + escHtml(source || '') + '\')" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
|
|
119
|
+
'</div>' +
|
|
120
|
+
'</div>';
|
|
121
|
+
document.getElementById('modal').classList.add('open');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function submitWorkItemEdit(id, source) {
|
|
125
|
+
const title = document.getElementById('wi-edit-title').value.trim();
|
|
126
|
+
const description = document.getElementById('wi-edit-desc').value;
|
|
127
|
+
const type = document.getElementById('wi-edit-type').value;
|
|
128
|
+
const priority = document.getElementById('wi-edit-priority').value;
|
|
129
|
+
const agent = document.getElementById('wi-edit-agent').value;
|
|
130
|
+
if (!title) { alert('Title is required'); return; }
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch('/api/work-items/update', {
|
|
133
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent })
|
|
135
|
+
});
|
|
136
|
+
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Work item updated', true); } else {
|
|
137
|
+
const d = await res.json();
|
|
138
|
+
alert('Update failed: ' + (d.error || 'unknown'));
|
|
139
|
+
}
|
|
140
|
+
} catch (e) { alert('Update error: ' + e.message); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function deleteWorkItem(id, source) {
|
|
144
|
+
if (!confirm('Delete work item ' + id + '? This will kill any running agent and remove all dispatch history.')) return;
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch('/api/work-items/delete', {
|
|
147
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
149
|
+
});
|
|
150
|
+
if (res.ok) { refresh(); } else {
|
|
151
|
+
const d = await res.json();
|
|
152
|
+
alert('Delete failed: ' + (d.error || 'unknown'));
|
|
153
|
+
}
|
|
154
|
+
} catch (e) { alert('Delete error: ' + e.message); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function archiveWorkItem(id, source) {
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch('/api/work-items/archive', {
|
|
160
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
162
|
+
});
|
|
163
|
+
if (res.ok) { refresh(); } else {
|
|
164
|
+
const d = await res.json();
|
|
165
|
+
alert('Archive failed: ' + (d.error || 'unknown'));
|
|
166
|
+
}
|
|
167
|
+
} catch (e) { alert('Archive error: ' + e.message); }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let wiArchiveVisible = false;
|
|
171
|
+
async function toggleWorkItemArchive() {
|
|
172
|
+
const el = document.getElementById('work-items-archive');
|
|
173
|
+
wiArchiveVisible = !wiArchiveVisible;
|
|
174
|
+
if (!wiArchiveVisible) { el.style.display = 'none'; return; }
|
|
175
|
+
el.style.display = 'block';
|
|
176
|
+
el.innerHTML = '<p class="empty">Loading archive...</p>';
|
|
177
|
+
try {
|
|
178
|
+
const items = await fetch('/api/work-items/archive').then(r => r.json());
|
|
179
|
+
if (!items.length) { el.innerHTML = '<p class="empty">No archived work items.</p>'; return; }
|
|
180
|
+
el.innerHTML = '<div style="font-size:10px;color:var(--muted);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Archived (' + items.length + ')</div>' +
|
|
181
|
+
'<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Agent</th><th>Archived</th></tr></thead><tbody>' +
|
|
182
|
+
items.map(function(i) {
|
|
183
|
+
return '<tr style="opacity:0.6">' +
|
|
184
|
+
'<td><span class="pr-id">' + escHtml(i.id || '') + '</span></td>' +
|
|
185
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(i.title || '') + '</td>' +
|
|
186
|
+
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escHtml(i.type || '') + '</span></td>' +
|
|
187
|
+
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(i.status || '') + '</td>' +
|
|
188
|
+
'<td>' + escHtml(i.dispatched_to || '—') + '</td>' +
|
|
189
|
+
'<td class="pr-date">' + shortTime(i.archivedAt) + '</td>' +
|
|
190
|
+
'</tr>';
|
|
191
|
+
}).join('') + '</tbody></table></div>';
|
|
192
|
+
} catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function retryWorkItem(id, source) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch('/api/work-items/retry', {
|
|
198
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
200
|
+
});
|
|
201
|
+
if (res.ok) { refresh(); } else {
|
|
202
|
+
const d = await res.json();
|
|
203
|
+
alert('Retry failed: ' + (d.error || 'unknown'));
|
|
204
|
+
}
|
|
205
|
+
} catch (e) { alert('Retry error: ' + e.message); }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
209
|
+
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
|
|
210
|
+
|
|
211
|
+
function openAllWorkItems() {
|
|
212
|
+
document.getElementById('modal-title').textContent = 'All Work Items (' + allWorkItems.length + ')';
|
|
213
|
+
const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>' +
|
|
214
|
+
allWorkItems.map(wiRow).join('') + '</tbody></table></div>';
|
|
215
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
216
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
217
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
218
|
+
document.getElementById('modal').classList.add('open');
|
|
219
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// settings.js — Settings panel functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
async function openSettings() {
|
|
4
|
+
let data;
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch('/api/settings');
|
|
7
|
+
data = await res.json();
|
|
8
|
+
} catch (e) { showToast('cmd-toast', 'Failed to load settings: ' + e.message, false); return; }
|
|
9
|
+
|
|
10
|
+
const e = data.engine || {};
|
|
11
|
+
const c = data.claude || {};
|
|
12
|
+
const agents = data.agents || {};
|
|
13
|
+
|
|
14
|
+
const agentRows = Object.entries(agents).map(function([id, a]) {
|
|
15
|
+
return '<tr>' +
|
|
16
|
+
'<td style="font-weight:600">' + escHtml(a.emoji || '') + ' ' + escHtml(a.name || id) + '</td>' +
|
|
17
|
+
'<td><input data-agent="' + escHtml(id) + '" data-field="role" value="' + escHtml(a.role || '') + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
|
|
18
|
+
'<td><input data-agent="' + escHtml(id) + '" data-field="skills" value="' + escHtml((a.skills || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
|
|
19
|
+
'</tr>';
|
|
20
|
+
}).join('');
|
|
21
|
+
|
|
22
|
+
const html = '<div style="padding:8px 0;max-height:70vh;overflow-y:auto">' +
|
|
23
|
+
|
|
24
|
+
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Engine</h3>' +
|
|
25
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
|
|
26
|
+
settingsField('Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'How often the engine runs discovery + dispatch') +
|
|
27
|
+
settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 3, '', 'Max agents working simultaneously') +
|
|
28
|
+
settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
|
|
29
|
+
settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
|
|
30
|
+
settingsField('Max Turns', 'set-maxTurns', e.maxTurns || 100, '', 'Claude CLI --max-turns per agent') +
|
|
31
|
+
settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
|
|
32
|
+
settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
|
|
33
|
+
settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
|
|
34
|
+
'</div>' +
|
|
35
|
+
|
|
36
|
+
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
|
|
37
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
|
|
38
|
+
settingsField('Output Format', 'set-outputFormat', c.outputFormat || 'stream-json', '', '') +
|
|
39
|
+
settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Comma-separated (empty = all)') +
|
|
40
|
+
'</div>' +
|
|
41
|
+
|
|
42
|
+
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
|
|
43
|
+
'<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:11px">' +
|
|
44
|
+
'<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th></tr>' +
|
|
45
|
+
agentRows +
|
|
46
|
+
'</table>' +
|
|
47
|
+
|
|
48
|
+
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Routing Table</h3>' +
|
|
49
|
+
'<textarea id="set-routing" rows="12" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:11px;resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
|
|
50
|
+
|
|
51
|
+
'<span id="settings-status" style="font-size:11px;color:var(--muted)"></span>' +
|
|
52
|
+
'</div>';
|
|
53
|
+
|
|
54
|
+
document.getElementById('modal-title').textContent = 'Settings';
|
|
55
|
+
|
|
56
|
+
// Add save button to modal header actions (next to copy/close)
|
|
57
|
+
const actions = document.querySelector('.modal-header-actions');
|
|
58
|
+
const existingSaveBtn = document.getElementById('modal-settings-save');
|
|
59
|
+
if (!existingSaveBtn) {
|
|
60
|
+
const saveBtn = document.createElement('button');
|
|
61
|
+
saveBtn.id = 'modal-settings-save';
|
|
62
|
+
saveBtn.className = 'modal-copy';
|
|
63
|
+
saveBtn.style.cssText = 'color:var(--green);border-color:var(--green)';
|
|
64
|
+
saveBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save';
|
|
65
|
+
saveBtn.onclick = saveSettings;
|
|
66
|
+
// Insert before the close button (last child)
|
|
67
|
+
actions.insertBefore(saveBtn, actions.lastElementChild);
|
|
68
|
+
}
|
|
69
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
70
|
+
document.getElementById('modal-body').style.fontFamily = '';
|
|
71
|
+
document.getElementById('modal-body').style.whiteSpace = '';
|
|
72
|
+
document.getElementById('modal').classList.add('open');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function settingsField(label, id, value, unit, hint) {
|
|
76
|
+
return '<div>' +
|
|
77
|
+
'<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
|
|
78
|
+
'<input id="' + id + '" value="' + escHtml(String(value)) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
|
|
79
|
+
(hint ? '<div style="font-size:9px;color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
|
|
80
|
+
'</div>';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function saveSettings() {
|
|
84
|
+
const status = document.getElementById('settings-status');
|
|
85
|
+
status.textContent = 'Saving...';
|
|
86
|
+
status.style.color = 'var(--blue)';
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const enginePayload = {
|
|
90
|
+
tickInterval: document.getElementById('set-tickInterval').value,
|
|
91
|
+
maxConcurrent: document.getElementById('set-maxConcurrent').value,
|
|
92
|
+
inboxConsolidateThreshold: document.getElementById('set-inboxConsolidateThreshold').value,
|
|
93
|
+
agentTimeout: document.getElementById('set-agentTimeout').value,
|
|
94
|
+
maxTurns: document.getElementById('set-maxTurns').value,
|
|
95
|
+
heartbeatTimeout: document.getElementById('set-heartbeatTimeout').value,
|
|
96
|
+
worktreeCreateTimeout: document.getElementById('set-worktreeCreateTimeout').value,
|
|
97
|
+
worktreeCreateRetries: document.getElementById('set-worktreeCreateRetries').value,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const claudePayload = {
|
|
101
|
+
outputFormat: document.getElementById('set-outputFormat').value,
|
|
102
|
+
allowedTools: document.getElementById('set-allowedTools').value,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const agentsPayload = {};
|
|
106
|
+
document.querySelectorAll('[data-agent][data-field]').forEach(function(el) {
|
|
107
|
+
const id = el.dataset.agent;
|
|
108
|
+
const field = el.dataset.field;
|
|
109
|
+
if (!agentsPayload[id]) agentsPayload[id] = {};
|
|
110
|
+
agentsPayload[id][field] = el.value;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Save config
|
|
114
|
+
const res = await fetch('/api/settings', {
|
|
115
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload })
|
|
117
|
+
});
|
|
118
|
+
const result = await res.json();
|
|
119
|
+
if (!res.ok) throw new Error(result.error);
|
|
120
|
+
|
|
121
|
+
// Save routing separately
|
|
122
|
+
const routing = document.getElementById('set-routing').value;
|
|
123
|
+
const rRes = await fetch('/api/settings/routing', {
|
|
124
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ content: routing })
|
|
126
|
+
});
|
|
127
|
+
if (!rRes.ok) { const d = await rRes.json(); throw new Error(d.error); }
|
|
128
|
+
|
|
129
|
+
status.textContent = 'Saved. Restart engine for full effect.';
|
|
130
|
+
status.style.color = 'var(--green)';
|
|
131
|
+
} catch (e) {
|
|
132
|
+
status.textContent = 'Error: ' + e.message;
|
|
133
|
+
status.style.color = 'var(--red)';
|
|
134
|
+
}
|
|
135
|
+
}
|