Changeset 0.27.4 (#171)

This commit is contained in:
2026-03-11 10:30:12 +00:00
parent a46ec63464
commit 07432233f7
13 changed files with 736 additions and 57 deletions

View File

@@ -25,6 +25,7 @@
'<div class="form-row" style="gap:12px">' +
'<label class="toggle-label"><input type="checkbox" id="taskCfgEnabled"><span class="toggle-track"></span><span>Tasks Enabled</span></label>' +
'<label class="toggle-label"><input type="checkbox" id="taskCfgPersonal"><span class="toggle-track"></span><span>Allow Personal Tasks</span></label>' +
'<label class="toggle-label"><input type="checkbox" id="taskCfgRequireBYOK"><span class="toggle-track"></span><span>Require BYOK for Personal</span></label>' +
'</div>' +
'<div class="form-row" style="gap:12px;margin-top:12px">' +
'<div class="form-group" style="flex:1"><label>Max Concurrent</label>' +
@@ -74,7 +75,7 @@
async function loadTasks() {
try {
var resp = await App.api.get('/api/v1/admin/tasks');
var resp = await API._get('/api/v1/admin/tasks');
var tasks = resp.data || [];
var list = document.getElementById('taskAdminList');
document.getElementById('taskCount').textContent = tasks.length;
@@ -127,21 +128,21 @@
async function triggerRun(taskId) {
try {
await App.api.post('/api/v1/admin/tasks/' + taskId + '/run', {});
App.showToast('Task scheduled for immediate execution', 'success');
await API._post('/api/v1/admin/tasks/' + taskId + '/run', {});
UI.toast('Task scheduled for immediate execution', 'success');
} catch (err) {
App.showToast(err.message || 'Failed to trigger run', 'error');
UI.toast(err.message || 'Failed to trigger run', 'error');
}
}
async function deleteTask(taskId) {
if (!confirm('Delete this task and all run history?')) return;
try {
await App.api.delete('/api/v1/admin/tasks/' + taskId);
App.showToast('Task deleted', 'success');
await API._delete('/api/v1/admin/tasks/' + taskId);
UI.toast('Task deleted', 'success');
loadTasks();
} catch (err) {
App.showToast(err.message || 'Failed to delete task', 'error');
UI.toast(err.message || 'Failed to delete task', 'error');
}
}
@@ -152,7 +153,7 @@
document.getElementById('taskRunTitle').textContent = 'Run History: ' + taskName;
showTab('runs');
try {
var resp = await App.api.get('/api/v1/tasks/' + taskId + '/runs');
var resp = await API._get('/api/v1/tasks/' + taskId + '/runs');
var runs = resp.data || [];
var list = document.getElementById('taskRunList');
@@ -190,20 +191,21 @@
async function killRun() {
if (!_currentTaskId) return;
try {
await App.api.post('/api/v1/admin/tasks/' + _currentTaskId + '/kill', {});
App.showToast('Active run cancelled', 'success');
await API._post('/api/v1/admin/tasks/' + _currentTaskId + '/kill', {});
UI.toast('Active run cancelled', 'success');
showRuns(_currentTaskId, document.getElementById('taskRunTitle').textContent.replace('Run History: ', ''));
} catch (err) {
App.showToast(err.message || 'Failed to kill run', 'error');
UI.toast(err.message || 'Failed to kill run', 'error');
}
}
async function loadConfig() {
try {
var resp = await App.api.get('/api/v1/admin/settings/tasks');
var resp = await API._get('/api/v1/admin/settings/tasks');
var cfg = resp || {};
document.getElementById('taskCfgEnabled').checked = cfg.enabled !== false;
document.getElementById('taskCfgPersonal').checked = cfg.allow_personal !== false;
document.getElementById('taskCfgRequireBYOK').checked = cfg.personal_require_byok === true;
document.getElementById('taskCfgMaxConcurrent').value = cfg.max_concurrent || 5;
document.getElementById('taskCfgMaxTokens').value = cfg.default_max_tokens || 4096;
document.getElementById('taskCfgMaxToolCalls').value = cfg.default_max_tool_calls || 10;
@@ -211,6 +213,7 @@
} catch (_) {
document.getElementById('taskCfgEnabled').checked = true;
document.getElementById('taskCfgPersonal').checked = true;
document.getElementById('taskCfgRequireBYOK').checked = false;
document.getElementById('taskCfgMaxConcurrent').value = 5;
document.getElementById('taskCfgMaxTokens').value = 4096;
document.getElementById('taskCfgMaxToolCalls').value = 10;
@@ -223,15 +226,16 @@
var payload = {
enabled: document.getElementById('taskCfgEnabled').checked,
allow_personal: document.getElementById('taskCfgPersonal').checked,
personal_require_byok: document.getElementById('taskCfgRequireBYOK').checked,
max_concurrent: parseInt(document.getElementById('taskCfgMaxConcurrent').value) || 5,
default_max_tokens: parseInt(document.getElementById('taskCfgMaxTokens').value) || 4096,
default_max_tool_calls: parseInt(document.getElementById('taskCfgMaxToolCalls').value) || 10,
default_max_wall_clock: parseInt(document.getElementById('taskCfgMaxWallClock').value) || 300
};
await App.api.put('/api/v1/admin/settings/tasks', payload);
App.showToast('Task configuration saved', 'success');
await API._put('/api/v1/admin/settings/tasks', payload);
UI.toast('Task configuration saved', 'success');
} catch (err) {
App.showToast(err.message || 'Failed to save config', 'error');
UI.toast(err.message || 'Failed to save config', 'error');
}
}

281
src/js/task-settings.js Normal file
View File

@@ -0,0 +1,281 @@
// ==========================================
// Chat Switchboard — Task Settings (User)
// ==========================================
// Settings → Tasks section. CRUD for personal tasks with schedule builder,
// persona/model picker, budget config, and starter templates.
// Loaded on the settings surface.
(function() {
'use strict';
const PRESETS = [
{ label: 'Every morning (6am)', cron: '0 6 * * *' },
{ label: 'Weekday mornings (8am)', cron: '0 8 * * 1-5' },
{ label: 'Every hour', cron: '0 * * * *' },
{ label: 'Weekly (Monday 9am)', cron: '0 9 * * 1' },
{ label: 'Monthly (1st midnight)', cron: '0 0 1 * *' },
{ label: 'Custom...', cron: '' },
];
const TEMPLATES = [
{ name: 'Morning News Digest', prompt: 'Summarize the top 5 tech news headlines from today. Be concise — one paragraph per story.', schedule: '0 6 * * *' },
{ name: 'Daily Standup Prep', prompt: 'Review my recent notes and conversations. Generate 3 concise standup talking points covering what I worked on yesterday, what I plan today, and any blockers.', schedule: '0 8 * * 1-5' },
{ name: 'Weekly Project Summary', prompt: 'Summarize this week\'s activity across my projects. Highlight completed items, open threads, and priorities for next week.', schedule: '0 17 * * 5' },
{ name: 'Stock Watchlist Check', prompt: 'Check for unusual pre-market activity on major tech stocks (AAPL, GOOGL, MSFT, NVDA, AMZN). Flag any moves > 2%.', schedule: '0 9 * * 1-5' },
{ name: 'Research Digest', prompt: 'Search for the latest papers and blog posts on AI agents and autonomous systems. Summarize the 3 most interesting findings.', schedule: '0 12 * * 1' },
];
function esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
// Detect browser timezone
function browserTZ() {
try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (_) { return 'UTC'; }
}
// ── Task list ────────────────────────────
async function loadTaskList() {
var mount = document.getElementById('settingsTasksMount');
if (!mount) return;
try {
// Load personal tasks
var resp = await API._get('/api/v1/tasks');
var personalTasks = (resp.data || []).map(function(t) { t._source = 'personal'; return t; });
// Load team tasks from all user's teams
var teamTasks = [];
try {
var teamsResp = await API._get('/api/v1/teams/mine');
var teams = teamsResp.data || teamsResp || [];
for (var i = 0; i < teams.length; i++) {
try {
var tr = await API._get('/api/v1/teams/' + teams[i].id + '/tasks');
(tr.data || []).forEach(function(t) {
t._source = 'team';
t._teamName = teams[i].name;
teamTasks.push(t);
});
} catch (_) { /* team may not have tasks */ }
}
} catch (_) { /* no teams */ }
// Deduplicate (user might own a team task)
var seen = {};
personalTasks.forEach(function(t) { seen[t.id] = true; });
teamTasks = teamTasks.filter(function(t) { return !seen[t.id]; });
var tasks = personalTasks.concat(teamTasks);
var html = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">' +
'<p style="color:var(--text-2);font-size:13px;margin:0">Your scheduled tasks. Tasks run automatically on their schedule.</p>' +
'<button class="btn-small btn-primary" id="taskNewBtn">+ New Task</button>' +
'</div>';
if (tasks.length > 0) {
html += tasks.map(function(t) {
var status = t.is_active ? '<span class="badge badge-success">active</span>' : '<span class="badge badge-muted">paused</span>';
var teamBadge = t._source === 'team' ? '<span class="badge" style="background:var(--accent-dim);color:var(--accent)">' + esc(t._teamName || 'team') + '</span>' : '';
var sched = t.schedule === 'once' ? 'One-shot' : t.schedule;
var lastStatus = '';
if (t.last_run_at) {
lastStatus = ' \u00b7 last: ' + new Date(t.last_run_at).toLocaleString();
}
var nextRun = t.next_run_at ? new Date(t.next_run_at).toLocaleString() : '\u2014';
return '<div class="settings-section" style="padding:12px 16px;margin-bottom:8px">' +
'<div style="display:flex;align-items:center;gap:12px">' +
'<div style="flex:1">' +
'<div style="font-weight:600;font-size:14px">' + esc(t.name) + '</div>' +
'<div style="font-size:11px;color:var(--text-2);margin-top:2px">' + esc(sched) + ' (' + esc(t.timezone) + ')' + lastStatus + '</div>' +
'<div style="font-size:11px;color:var(--text-2)">next: ' + nextRun + '</div>' +
'</div>' +
'<div style="display:flex;gap:6px;align-items:center">' +
teamBadge + status +
'<button class="btn-small task-run-btn" data-id="' + t.id + '" title="Run Now">\u25b6</button>' +
'<button class="btn-small task-toggle-btn" data-id="' + t.id + '" data-active="' + t.is_active + '" title="' + (t.is_active ? 'Pause' : 'Resume') + '">' + (t.is_active ? '\u23f8' : '\u25b6') + '</button>' +
'<button class="btn-small btn-danger task-del-btn" data-id="' + t.id + '" title="Delete">\u2715</button>' +
'</div>' +
'</div>' +
'</div>';
}).join('');
}
// Templates section
html += '<div style="margin-top:24px"><h4 style="margin:0 0 8px">Starter Templates</h4>' +
'<p style="color:var(--text-2);font-size:12px;margin:0 0 12px">Quick-start with a pre-configured task. You can customize after creation.</p>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">';
TEMPLATES.forEach(function(tmpl) {
html += '<div class="settings-section" style="padding:10px 12px;cursor:pointer" onclick="window._createFromTemplate(\'' + esc(tmpl.name) + '\')">' +
'<div style="font-weight:600;font-size:13px">' + esc(tmpl.name) + '</div>' +
'<div style="font-size:11px;color:var(--text-2);margin-top:4px">' + esc(tmpl.schedule) + '</div>' +
'</div>';
});
html += '</div></div>';
mount.innerHTML = html;
// Wire buttons
document.getElementById('taskNewBtn').addEventListener('click', function() { showCreateForm(); });
mount.querySelectorAll('.task-run-btn').forEach(function(btn) {
btn.addEventListener('click', function() { runTask(btn.dataset.id); });
});
mount.querySelectorAll('.task-toggle-btn').forEach(function(btn) {
btn.addEventListener('click', function() { toggleTask(btn.dataset.id, btn.dataset.active === 'true'); });
});
mount.querySelectorAll('.task-del-btn').forEach(function(btn) {
btn.addEventListener('click', function() { deleteTask(btn.dataset.id); });
});
} catch (err) {
mount.innerHTML = '<p style="color:var(--danger)">Failed to load tasks: ' + esc(err.message) + '</p>';
}
}
// ── Create form ──────────────────────────
function showCreateForm(defaults) {
var d = defaults || {};
var mount = document.getElementById('settingsTasksMount');
if (!mount) return;
var schedOpts = PRESETS.map(function(p) {
var sel = (d.schedule && d.schedule === p.cron) ? ' selected' : '';
return '<option value="' + esc(p.cron) + '"' + sel + '>' + esc(p.label) + '</option>';
}).join('');
mount.innerHTML =
'<div style="margin-bottom:16px"><button class="btn-small" id="taskBackBtn">\u2190 Back</button></div>' +
'<div class="settings-section" style="padding:16px">' +
'<div class="form-row" style="gap:12px">' +
'<div class="form-group" style="flex:2"><label>Name</label>' +
'<input type="text" id="taskName" value="' + esc(d.name || '') + '" style="width:100%" placeholder="Morning News Digest"></div>' +
'<div class="form-group" style="flex:1"><label>Model</label>' +
'<input type="text" id="taskModel" value="' + esc(d.model || '') + '" style="width:100%" placeholder="(use default)"></div>' +
'</div>' +
'<div class="form-group" style="margin-top:8px"><label>Prompt</label>' +
'<textarea id="taskPrompt" rows="4" style="width:100%">' + esc(d.prompt || '') + '</textarea></div>' +
'<div class="form-row" style="gap:12px;margin-top:8px">' +
'<div class="form-group" style="flex:1"><label>Schedule</label>' +
'<select id="taskPreset" style="width:100%">' + schedOpts + '</select></div>' +
'<div class="form-group" style="flex:1" id="taskCustomCronGroup" style="display:none"><label>Custom Cron</label>' +
'<input type="text" id="taskCron" value="' + esc(d.schedule || '') + '" style="width:100%" placeholder="0 6 * * *"></div>' +
'<div class="form-group" style="flex:1"><label>Timezone</label>' +
'<input type="text" id="taskTZ" value="' + esc(d.timezone || browserTZ()) + '" style="width:100%"></div>' +
'</div>' +
'<div class="form-row" style="gap:12px;margin-top:8px">' +
'<div class="form-group" style="flex:1"><label>Output Mode</label>' +
'<select id="taskOutput" style="width:100%"><option value="channel">Channel</option><option value="note">Note</option><option value="webhook">Webhook</option></select></div>' +
'<div class="form-group" style="flex:1" id="taskWebhookGroup" style="display:none"><label>Webhook URL</label>' +
'<input type="text" id="taskWebhookURL" style="width:100%" placeholder="https://..."></div>' +
'</div>' +
'<div class="form-row" style="gap:12px;margin-top:8px">' +
'<div class="form-group" style="flex:1"><label>Max Tokens</label>' +
'<input type="number" id="taskMaxTokens" value="" style="width:100%" placeholder="default"></div>' +
'<div class="form-group" style="flex:1"><label>Max Tool Calls</label>' +
'<input type="number" id="taskMaxToolCalls" value="" style="width:100%" placeholder="default"></div>' +
'</div>' +
'<div style="display:flex;gap:8px;margin-top:12px;align-items:center">' +
'<label class="toggle-label"><input type="checkbox" id="taskNotifyComplete"><span class="toggle-track"></span><span>Notify on complete</span></label>' +
'<label class="toggle-label"><input type="checkbox" id="taskNotifyFail" checked><span class="toggle-track"></span><span>Notify on failure</span></label>' +
'</div>' +
'<button class="btn-small btn-primary" id="taskCreateBtn" style="margin-top:12px">Create Task</button>' +
'</div>';
document.getElementById('taskBackBtn').addEventListener('click', loadTaskList);
document.getElementById('taskCreateBtn').addEventListener('click', createTask);
// Toggle custom cron visibility
var preset = document.getElementById('taskPreset');
var customGroup = document.getElementById('taskCustomCronGroup');
preset.addEventListener('change', function() {
customGroup.style.display = preset.value === '' ? '' : 'none';
if (preset.value !== '') document.getElementById('taskCron').value = preset.value;
});
// Initial state
customGroup.style.display = preset.value === '' ? '' : 'none';
if (preset.value !== '') document.getElementById('taskCron').value = preset.value;
// Toggle webhook URL visibility
var output = document.getElementById('taskOutput');
var webhookGroup = document.getElementById('taskWebhookGroup');
output.addEventListener('change', function() {
webhookGroup.style.display = output.value === 'webhook' ? '' : 'none';
});
}
async function createTask() {
var schedule = document.getElementById('taskCron').value || document.getElementById('taskPreset').value;
if (!schedule) { UI.toast('Schedule is required', 'error'); return; }
var payload = {
name: document.getElementById('taskName').value,
task_type: 'prompt',
user_prompt: document.getElementById('taskPrompt').value,
schedule: schedule,
timezone: document.getElementById('taskTZ').value || 'UTC',
output_mode: document.getElementById('taskOutput').value,
notify_on_complete: document.getElementById('taskNotifyComplete').checked,
notify_on_failure: document.getElementById('taskNotifyFail').checked,
};
var model = document.getElementById('taskModel').value;
if (model) payload.model_id = model;
var maxTokens = parseInt(document.getElementById('taskMaxTokens').value);
if (maxTokens > 0) payload.max_tokens = maxTokens;
var maxTools = parseInt(document.getElementById('taskMaxToolCalls').value);
if (maxTools > 0) payload.max_tool_calls = maxTools;
var webhookURL = document.getElementById('taskWebhookURL')?.value;
if (payload.output_mode === 'webhook' && webhookURL) {
payload.webhook_url = webhookURL;
}
try {
await API._post('/api/v1/tasks', payload);
UI.toast('Task created', 'success');
loadTaskList();
// Refresh sidebar
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
} catch (err) {
UI.toast(err.message || 'Failed to create task', 'error');
}
}
// ── Actions ──────────────────────────────
async function runTask(id) {
try {
await API._post('/api/v1/tasks/' + id + '/run', {});
UI.toast('Task scheduled for immediate run', 'success');
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
}
async function toggleTask(id, isActive) {
try {
await API._put('/api/v1/tasks/' + id, { is_active: !isActive });
UI.toast(isActive ? 'Task paused' : 'Task resumed', 'success');
loadTaskList();
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
}
async function deleteTask(id) {
if (!confirm('Delete this task?')) return;
try {
await API._delete('/api/v1/tasks/' + id);
UI.toast('Task deleted', 'success');
loadTaskList();
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
}
// Template launcher
window._createFromTemplate = function(name) {
var tmpl = TEMPLATES.find(function(t) { return t.name === name; });
if (tmpl) showCreateForm({ name: tmpl.name, prompt: tmpl.prompt, schedule: tmpl.schedule });
};
// ── Entry point (called from settings loader) ──
window._loadSettingsTasks = function() {
loadTaskList();
};
})();

143
src/js/task-sidebar.js Normal file
View File

@@ -0,0 +1,143 @@
// ==========================================
// Chat Switchboard — Task Sidebar Section
// ==========================================
// Sidebar section showing user's scheduled tasks with status indicators.
// Click opens the task's output channel. Same pattern as WorkflowQueue.
(function() {
'use strict';
window.TaskSidebar = {
_loaded: false,
// ── Init ────────────────────────────
async init() {
if (this._loaded) return;
this._loaded = true;
// Insert after Queue section (or after Channels if no queue)
var anchor = document.getElementById('sbSectionQueue') ||
document.getElementById('sbSectionChannels');
if (!anchor) return;
var section = document.createElement('div');
section.className = 'sb-section';
section.id = 'sbSectionTasks';
section.innerHTML =
'<div class="sb-section-header" onclick="UI.toggleSidebarSection(\'tasks\')">' +
'<span class="sb-section-arrow" id="sbArrowTasks">\u25be</span>' +
'<span class="sb-section-label">Tasks</span>' +
'<span class="sb-queue-badge" id="sbTaskBadge" style="display:none">0</span>' +
'</div>' +
'<div class="sb-section-body" id="sbBodyTasks"></div>';
anchor.parentNode.insertBefore(section, anchor.nextSibling);
this.refresh();
},
// ── Refresh ─────────────────────────
async refresh() {
var body = document.getElementById('sbBodyTasks');
var badge = document.getElementById('sbTaskBadge');
if (!body) return;
try {
// Personal tasks
var resp = await API._get('/api/v1/tasks');
var tasks = (resp.data || []).filter(function(t) { return t.is_active; });
// Team tasks (v0.27.5)
try {
var teamsResp = await API._get('/api/v1/teams/mine');
var teams = teamsResp.data || teamsResp || [];
var seen = {};
tasks.forEach(function(t) { seen[t.id] = true; });
for (var i = 0; i < teams.length; i++) {
try {
var tr = await API._get('/api/v1/teams/' + teams[i].id + '/tasks');
(tr.data || []).forEach(function(t) {
if (!seen[t.id] && t.is_active) {
t._teamName = teams[i].name;
tasks.push(t);
seen[t.id] = true;
}
});
} catch (_) {}
}
} catch (_) {}
if (tasks.length === 0) {
body.innerHTML = '';
if (badge) badge.style.display = 'none';
return;
}
if (badge) {
badge.textContent = tasks.length;
badge.style.display = '';
}
var html = '';
tasks.forEach(function(t) {
// Status indicator
var icon = '\u25f7'; // default: clock
var statusTip = 'Scheduled';
if (t.last_run_at) {
// Check if most recent run was recent (within 2x schedule interval)
icon = '\u2713'; // checkmark
statusTip = 'Last: ' + new Date(t.last_run_at).toLocaleString();
}
var nextTip = t.next_run_at
? 'Next: ' + new Date(t.next_run_at).toLocaleString()
: '';
var channelId = t.output_channel_id || '';
var onclick = channelId
? 'selectChannel(\'' + channelId + '\')'
: 'UI.toast(\'No output channel yet — task has not run\',\'info\')';
var label = _esc(t.name);
if (t._teamName) label = '<span style="opacity:0.6;font-size:10px">[' + _esc(t._teamName) + ']</span> ' + label;
html += '<div class="sb-queue-item" onclick="' + onclick + '" title="' +
_esc(statusTip + (nextTip ? ' \u00b7 ' + nextTip : '')) + '">' +
'<span class="sb-queue-icon">' + icon + '</span>' +
'<span class="sb-queue-label">' + label + '</span>' +
'<button class="btn-small" style="padding:0 4px;font-size:10px" ' +
'onclick="event.stopPropagation();TaskSidebar.runNow(\'' + t.id + '\')" title="Run Now">\u25b6</button>' +
'</div>';
});
body.innerHTML = html;
} catch (err) {
body.innerHTML = '';
if (badge) badge.style.display = 'none';
}
},
// ── Run Now ─────────────────────────
async runNow(taskId) {
try {
await API._post('/api/v1/tasks/' + taskId + '/run', {});
UI.toast('Task triggered', 'success');
} catch (err) {
UI.toast(err.message || 'Failed to run task', 'error');
}
}
};
function _esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
// Auto-init when chat surface loads
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (typeof API !== 'undefined' && API.accessToken) {
TaskSidebar.init();
}
}, 2200); // After WorkflowQueue (2000ms)
});
})();

View File

@@ -630,6 +630,9 @@ const UI = {
// Refresh workflow queue section (workflow channels filtered out of chat list)
if (typeof WorkflowQueue !== 'undefined') WorkflowQueue.refresh();
// Refresh task sidebar section (v0.27.4)
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
},
toggleFolder(folderId) {