Polish email tasks and window controls
This commit is contained in:
@@ -23,7 +23,7 @@ const DAYS_OF_WEEK = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'S
|
||||
|
||||
async function _fetchTasks() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tasks?include_last_run=true`, { credentials: 'same-origin' });
|
||||
const res = await fetch(`${API_BASE}/api/tasks`, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
_tasks = data.tasks || [];
|
||||
} catch (e) {
|
||||
@@ -127,6 +127,21 @@ async function _runNow(id, force = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function _stopTask(id) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/${id}/stop`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!res.ok) {
|
||||
let msg = `Failed to stop task (${res.status})`;
|
||||
try {
|
||||
const data = await res.json();
|
||||
if (data && data.detail) msg = data.detail;
|
||||
} catch (_) {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function _fetchRuns(taskId, limit = 10) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/runs?limit=${limit}`, {
|
||||
credentials: 'same-origin',
|
||||
@@ -568,6 +583,19 @@ function _renderTaskChips() {
|
||||
for (const c of cats) mkChip(`${c} (${counts[c]})`, c, _taskFilter === c);
|
||||
}
|
||||
|
||||
const _TASK_CACHE_LABELS = {
|
||||
summarize_emails: 'email summaries',
|
||||
draft_email_replies: 'AI reply drafts',
|
||||
extract_email_events: 'email calendar cache',
|
||||
mark_email_boundaries: 'email boundaries',
|
||||
learn_sender_signatures: 'sender signatures',
|
||||
check_email_urgency: 'email tags',
|
||||
};
|
||||
|
||||
function _taskClearCacheLabel(taskOrEntry) {
|
||||
return _TASK_CACHE_LABELS[taskOrEntry?.action || ''] || '';
|
||||
}
|
||||
|
||||
function _renderList() {
|
||||
const list = document.getElementById('tasks-list');
|
||||
if (!list) return;
|
||||
@@ -630,7 +658,7 @@ function _renderList() {
|
||||
const statusBadge = task.status === 'paused'
|
||||
? `<span class="task-status-badge task-paused-badge" data-task-status-action="resume" title="Click to resume" style="position:relative;top:4px;"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg> paused</span>`
|
||||
: task.status === 'active'
|
||||
? `<span class="task-status-badge task-active-badge" data-task-status-action="pause" title="Click to pause" style="position:relative;top:4px;"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><polygon points="7 4 19 12 7 20 7 4"/></svg> active</span>`
|
||||
? `<span class="task-status-badge task-active-badge" data-task-status-action="pause" title="Click to pause" style="position:relative;top:4px;">active</span>`
|
||||
: '';
|
||||
const builtinBadge = task.is_builtin
|
||||
? `<span class="task-builtin-badge${task.is_modified ? ' modified' : ''}" title="${task.is_modified ? 'Built-in task — edited from its default' : 'Built-in task'}">built-in${task.is_modified ? ' · edited' : ''}</span>`
|
||||
@@ -659,6 +687,9 @@ function _renderList() {
|
||||
if (task.is_builtin && task.is_modified) {
|
||||
items.push({ label: 'Revert to default', icon: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>', action: () => _doRevert(task.id) });
|
||||
}
|
||||
if (_taskClearCacheLabel(task)) {
|
||||
items.push({ label: 'Clear cache', icon: '<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5"/><path d="M14 11v5"/>', action: () => _doClearTaskCache(task.id, _taskClearCacheLabel(task)) });
|
||||
}
|
||||
items.push({ label: 'Delete', icon: '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/>', action: () => _doDelete(task.id), danger: true });
|
||||
_showTaskDropdown(menuBtn, items);
|
||||
});
|
||||
@@ -667,10 +698,10 @@ function _renderList() {
|
||||
// manual triggering. Hidden for completed tasks (same gate as before).
|
||||
if (task.status !== 'completed') {
|
||||
const runBtn = document.createElement('button');
|
||||
runBtn.className = 'memory-item-btn task-card-run-btn';
|
||||
runBtn.className = 'task-status-badge task-run-now-badge task-card-run-btn';
|
||||
runBtn.title = 'Run now';
|
||||
runBtn.style.cssText = 'position:relative;top:4px;margin-right:4px;display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 6px;';
|
||||
runBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg><span>Run</span>';
|
||||
runBtn.style.cssText = 'position:relative;top:1px;margin-right:4px;';
|
||||
runBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg><span>Run now</span>';
|
||||
runBtn.addEventListener('click', (e) => { e.stopPropagation(); _doRunNow(task.id); });
|
||||
actionsWrap.insertBefore(runBtn, menuBtn);
|
||||
}
|
||||
@@ -1578,6 +1609,25 @@ async function _doRevert(id) {
|
||||
} catch (e) { if (uiModule) uiModule.showError(e.message); }
|
||||
}
|
||||
|
||||
async function _doClearTaskCache(id, label = 'cache') {
|
||||
const ok = uiModule?.styledConfirm
|
||||
? await uiModule.styledConfirm(`Clear cached ${label} for this task?`, { confirmText: 'Clear' })
|
||||
: confirm(`Clear cached ${label} for this task?`);
|
||||
if (!ok) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/${encodeURIComponent(id)}/clear-cache`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);
|
||||
const n = Object.values(data.cleared || {}).reduce((a, b) => a + Number(b || 0), 0) + Number(data.files || 0);
|
||||
if (uiModule) uiModule.showToast(`Cleared ${label}${n ? ` (${n})` : ''}`);
|
||||
} catch (e) {
|
||||
if (uiModule) uiModule.showError(`Clear cache failed: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function _doToggleAll() {
|
||||
// If any task is active → pause all. Else resume all paused tasks.
|
||||
const hasActive = _tasks.some(t => t.status === 'active');
|
||||
@@ -1680,10 +1730,6 @@ async function _renderActivityView() {
|
||||
|
||||
document.getElementById('tasks-activity-refresh').addEventListener('click', _renderActivityView);
|
||||
|
||||
// Loading placeholder matches the document library: app whirlpool + label.
|
||||
const _actList = document.getElementById('tasks-activity-list');
|
||||
if (_actList) _actList.appendChild(spinnerModule.createLoadingRow('Loading…'));
|
||||
|
||||
// Solo filter: clicking a chip shows ONLY that group (a category, or
|
||||
// Errors). Clicking the active chip again clears the filter (show all).
|
||||
// At most one chip is active at a time. _solo holds the active key, or null.
|
||||
@@ -1771,6 +1817,14 @@ async function _renderActivityView() {
|
||||
const searchEl = document.getElementById('tasks-activity-search');
|
||||
if (searchEl) searchEl.addEventListener('input', () => { _afQuery = searchEl.value; _buildChips(); _applyFilter(); });
|
||||
|
||||
const _actList = document.getElementById('tasks-activity-list');
|
||||
if (_activityEntries.length) {
|
||||
_buildChips();
|
||||
_applyFilter();
|
||||
} else if (_actList) {
|
||||
_actList.appendChild(spinnerModule.createLoadingRow('Loading…'));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/runs/recent?limit=100`, { credentials: 'same-origin' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
@@ -1796,6 +1850,7 @@ async function _renderActivityView() {
|
||||
kind: r.task_type || 'llm',
|
||||
taskName: r.task_name || (r.task_type === 'action' ? (r.action || 'Action') : 'Task'),
|
||||
taskId: r.task_id,
|
||||
action: r.action || '',
|
||||
result: resultText,
|
||||
prompt: '',
|
||||
ts: r.finished_at || r.started_at,
|
||||
@@ -1916,9 +1971,9 @@ function _wireActivityRows(list) {
|
||||
// counter). No-op when there's nothing to tick.
|
||||
_startActivityTimers(list);
|
||||
list.querySelectorAll('.task-log-row').forEach(row => {
|
||||
// Click anywhere on the (non-running, non-skipped) row to toggle expand.
|
||||
// Click anywhere on the row to toggle expand.
|
||||
// Buttons inside still get their own handlers via stopPropagation.
|
||||
if (!row.classList.contains('is-running') && !row.classList.contains('is-skipped')) {
|
||||
if (!row.classList.contains('is-skipped')) {
|
||||
row.addEventListener('click', () => row.classList.toggle('expanded'));
|
||||
}
|
||||
row.querySelector('.task-log-row-toggle')?.addEventListener('click', (e) => {
|
||||
@@ -1943,6 +1998,25 @@ function _wireActivityRows(list) {
|
||||
const entry = _activityEntries[idx];
|
||||
if (entry?.taskId) _doRunNow(entry.taskId, true);
|
||||
});
|
||||
row.querySelector('.task-log-stop')?.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(row.dataset.entryIdx, 10);
|
||||
const entry = _activityEntries[idx];
|
||||
if (!entry?.taskId) return;
|
||||
try {
|
||||
await _stopTask(entry.taskId);
|
||||
uiModule.showToast('Task stopped');
|
||||
_renderActivityView();
|
||||
} catch (err) {
|
||||
uiModule.showError(err.message || 'Failed to stop task');
|
||||
}
|
||||
});
|
||||
row.querySelector('.task-log-run-again')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(row.dataset.entryIdx, 10);
|
||||
const entry = _activityEntries[idx];
|
||||
if (entry?.taskId) _doRunNow(entry.taskId);
|
||||
});
|
||||
row.querySelector('.task-log-copy')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(row.dataset.entryIdx, 10);
|
||||
@@ -1954,6 +2028,12 @@ function _wireActivityRows(list) {
|
||||
uiModule.showToast('Log copied');
|
||||
} catch (_) { uiModule.showError('Copy failed'); }
|
||||
});
|
||||
row.querySelector('.task-log-clear-cache')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(row.dataset.entryIdx, 10);
|
||||
const entry = _activityEntries[idx];
|
||||
if (entry?.taskId) _doClearTaskCache(entry.taskId, _taskClearCacheLabel(entry));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2113,13 +2193,11 @@ function _renderActivityEntry(entry) {
|
||||
const statusDot = `<span class="task-log-status task-log-status-${status}" title="${status}"></span>`;
|
||||
// Render the result through markdown so code blocks, lists, links look right.
|
||||
let resultHtml;
|
||||
// Running / queued rows: body stays empty — the status now lives on the
|
||||
// right side of the head row ("Running <whirlpool>"), wired below.
|
||||
const _isRunning = entry.status === 'running' || entry.status === 'queued';
|
||||
// Skipped (noop) rows: render as a slim, dimmed one-liner — no body, no
|
||||
// actions, just `· name · skipped — reason · time`. CSS via .is-skipped.
|
||||
const _isSkipped = entry.status === 'skipped';
|
||||
if (_isRunning) {
|
||||
if (_isRunning && !(entry.result || '').trim()) {
|
||||
resultHtml = '';
|
||||
} else {
|
||||
try {
|
||||
@@ -2155,6 +2233,7 @@ function _renderActivityEntry(entry) {
|
||||
// CSS vars feed the colored title + accent stripe.
|
||||
const styleVars = `--cat-hue:${hue};`;
|
||||
const hasResult = !!(entry.result && entry.result.trim() && entry.status !== 'running' && entry.status !== 'queued');
|
||||
const hasRunningProgress = !!(entry.result && entry.result.trim() && (entry.status === 'running' || entry.status === 'queued'));
|
||||
// "Open in chat" only makes sense for runs whose result is a real assistant
|
||||
// message (Prompt / Research tasks). Action/event runs are just log lines
|
||||
// (e.g. "No recent emails", "Tidied N memories") — for those, replace the
|
||||
@@ -2179,6 +2258,19 @@ function _renderActivityEntry(entry) {
|
||||
Copy log
|
||||
</button>`;
|
||||
}
|
||||
const clearLabel = _taskClearCacheLabel(entry);
|
||||
if (hasResult && clearLabel && entry.taskId) {
|
||||
actionBtn += `<button class="task-log-clear-cache" type="button" title="Clear cached ${_escHtml(clearLabel)} for this task">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5"/><path d="M14 11v5"/></svg>
|
||||
Clear cache
|
||||
</button>`;
|
||||
}
|
||||
if (hasResult && entry.taskId) {
|
||||
actionBtn += `<button class="task-log-run-again" type="button" title="Run this task again">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
Run again
|
||||
</button>`;
|
||||
}
|
||||
// Running rows replace the relative-time on the right with "Running NN" + a
|
||||
// live whirlpool spinner. Queued shows "Queued" the same way (no timer —
|
||||
// hasn't actually started yet). The elapsed counter ticks every second via
|
||||
@@ -2191,7 +2283,8 @@ function _renderActivityEntry(entry) {
|
||||
const startMs = entry.ts ? new Date(entry.ts).getTime() : Date.now();
|
||||
const elapsedInit = isQueued ? '' : `<span class="task-log-running-elapsed" data-since="${startMs}">${_fmtElapsed(Date.now() - startMs)}</span>`;
|
||||
const forceBtn = isQueued && entry.taskId ? `<button class="task-log-force-run" type="button" title="Start now in parallel, bypassing the queue" style="border:0;background:transparent;box-shadow:none;margin-left:5px;padding:0;width:12px;height:12px;display:inline-flex;align-items:center;justify-content:center;font-size:10px;line-height:1;color:inherit;opacity:.8;"><svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style="display:block;"><polygon points="6 4 20 12 6 20 6 4"/></svg></button>` : '';
|
||||
rightHtml = `<span class="task-log-running-inline"><span class="task-log-running-label">${label}</span>${elapsedInit}<span data-spin-here="1"></span>${forceBtn}</span>`;
|
||||
const stopBtn = entry.taskId ? `<button class="task-log-stop" type="button" title="Stop this task"><svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1"/></svg></button>` : '';
|
||||
rightHtml = `<span class="task-log-running-inline"><span class="task-log-running-label">${label}</span>${elapsedInit}<span data-spin-here="1"></span>${forceBtn}${stopBtn}</span>`;
|
||||
} else {
|
||||
rightHtml = `<span class="task-log-time" title="${_escHtml(tsAbs)}">${_escHtml(tsLabel)}</span>`;
|
||||
}
|
||||
@@ -2223,7 +2316,7 @@ function _renderActivityEntry(entry) {
|
||||
<span style="flex:1"></span>
|
||||
${rightHtml}
|
||||
</div>
|
||||
${_isRunning ? '' : `<div class="task-log-row-body">${resultHtml}</div>`}
|
||||
${(_isRunning && !hasRunningProgress) ? '' : `<div class="task-log-row-body">${resultHtml}</div>`}
|
||||
${promptHtml}
|
||||
<div class="task-log-row-actions">
|
||||
${long ? '<button class="task-log-row-toggle" type="button">Show more</button>' : '<span></span>'}
|
||||
|
||||
Reference in New Issue
Block a user