Merge claude/1

This commit is contained in:
librelad 2026-05-27 15:21:42 +01:00
commit 496c9ed1b3

View File

@ -2318,38 +2318,116 @@ class TasksManager {
}
async clearAllTasks() {
// Use the confirmation dialog system if available, otherwise fallback to confirm
const result = await this._showClearAllModal(this.tasks);
if (!result || !result.confirmed) return false;
await this.performClearAll({ cancelRunning: result.cancelRunning });
return true;
}
// Confirmation modal for clearAllTasks. Same openEoModal shape as
// _showDeleteTaskModal so visual identity matches every other destructive
// confirmation (Uninstall, Delete Task, …). Adds a "Cancel running tasks
// too" toggle (off by default) — when off, running/queued tasks are
// skipped; when on, they're cancelled first then deleted.
// Resolves {confirmed, cancelRunning}. Cancel/backdrop/close → confirmed=false.
_showClearAllModal(tasks) {
return new Promise((resolve) => {
if (window.showConfirmation) {
window.showConfirmation(
'Clear All Tasks',
'Are you sure you want to clear all tasks? This will delete all task history and cannot be undone.',
() => {
this.performClearAll();
resolve(true);
const escHtml = (s) => String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending');
const total = (tasks || []).length;
const runningCount = (tasks || []).filter(isActive).length;
const terminalCount = total - runningCount;
const bodyHtml = `
<div class="eo-empty-state danger" role="status">
<div class="eo-empty-state-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<div class="eo-empty-state-body">
<p class="eo-empty-state-title">This cannot be undone</p>
<p class="eo-empty-state-text">All selected tasks and their logs will be permanently removed.</p>
</div>
</div>
${window.eoBadgeRow ? window.eoBadgeRow([
{ label: `Total: ${total}`, variant: 'info' },
...(runningCount > 0 ? [{ label: `Running: ${runningCount}`, variant: 'warning' }] : []),
...(terminalCount > 0 ? [{ label: `Terminal: ${terminalCount}`, variant: 'success' }] : []),
]) : ''}
${runningCount > 0 ? `
<label class="eo-toggle eo-toggle-card" data-eo-toggle-row>
<input type="checkbox" id="clear-all-cancel-running">
<span class="eo-toggle-track"></span>
<span class="eo-toggle-text">
<span class="eo-toggle-text-title">Cancel running tasks too</span>
<span class="eo-toggle-text-help">Off: skip the ${runningCount} active task${runningCount === 1 ? '' : 's'}. On: cancel them first, then delete.</span>
</span>
</label>
` : ''}
`;
let decided = false;
const finish = (val, modal) => {
if (decided) return;
decided = true;
if (modal) modal.close();
resolve(val);
};
const m = window.openEoModal({
id: 'clear-all-tasks-modal',
size: 'sm',
eyebrow: 'Delete Tasks',
title: total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`,
desc: 'Confirm to delete the selected tasks.',
body: bodyHtml,
actions: [
{
label: 'Delete',
variant: 'danger',
onClick: (modal) => {
const cb = modal.bodyEl.querySelector('#clear-all-cancel-running');
finish({ confirmed: true, cancelRunning: !!(cb && cb.checked) }, modal);
}
},
'Yes, Clear All',
'Cancel',
'clear',
false
);
} else {
// Fallback to native confirm
const confirmed = confirm('Are you sure you want to clear all tasks? This will delete all task history.');
if (confirmed) {
this.performClearAll();
{ label: 'Cancel', variant: 'secondary', onClick: (modal) => finish({ confirmed: false }, modal) }
],
onClose: () => finish({ confirmed: false }, null)
});
// Live-update the danger button's label so the user knows exactly
// what will happen as they flip the toggle. No-op when no running
// tasks (no toggle in the body in that case).
const cb = m.bodyEl.querySelector('#clear-all-cancel-running');
const deleteBtn = m.contentEl.querySelector('.btn-danger');
const updateLabel = () => {
if (!deleteBtn) return;
const cancelRunning = !!(cb && cb.checked);
if (runningCount > 0 && !cancelRunning) {
deleteBtn.textContent = `Delete ${terminalCount} (skip ${runningCount} running)`;
} else {
deleteBtn.textContent = total === 1 ? 'Delete Task' : `Delete ${total} Tasks`;
}
resolve(confirmed);
}
};
if (cb) cb.addEventListener('change', updateLabel);
updateLabel();
});
}
async performClearAll() {
async performClearAll(opts) {
opts = opts || {};
const cancelRunning = !!opts.cancelRunning;
// Show progress notification with the trash type icon in the left slot
// (this is conceptually a delete-all action, so 🗑️ matches the row icon).
const customIcon = '<span style="font-size:18px;line-height:1;">🗑️</span>';
const progressNotification = window.notificationSystem.show(
'Clearing all tasks...',
'Clearing tasks...',
'info',
null,
null,
@ -2357,40 +2435,51 @@ class TasksManager {
customIcon
);
const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending');
// Partition: which tasks we attempt now, which we skip silently.
const targets = (this.tasks || []).filter(t => cancelRunning || !isActive(t));
const skipped = (this.tasks || []).filter(t => !targets.includes(t));
try {
// Delete all tasks using the task manager
const deletePromises = this.tasks.map(task =>
this.taskManager.deleteTask(task.id)
);
// For active tasks (only reached when the toggle is on): cancel first,
// wait for terminal status, then delete. Mirrors the single-row
// deleteTask flow so the bash processor has a chance to honour the
// cancel marker before the DELETE arrives (otherwise we'd 409).
await Promise.all(targets.map(async (task) => {
if (cancelRunning && isActive(task)) {
try { await this.taskManager.cancelTask(task.id); } catch {}
await this._waitForTaskTerminal(task.id, 15_000);
}
try {
await this.taskManager.deleteTask(task.id);
} catch (err) {
const looks409 = /\bHTTP 409\b/.test(err && err.message ? err.message : '');
if (!looks409) throw err;
await this.taskManager.deleteTask(task.id, { force: true });
}
}));
await Promise.all(deletePromises);
// Clear local array and re-render
this.tasks = [];
// Re-sync local state: keep anything we intentionally skipped.
const deletedIds = new Set(targets.map(t => t.id));
this.tasks = this.tasks.filter(t => !deletedIds.has(t.id));
this.renderTasks();
this.updateStats();
this.updateSidebarCounts();
this.generateAppCategories();
// Remove progress notification and show success
if (progressNotification && progressNotification.remove) {
progressNotification.remove();
}
if (window.notificationSystem) {
window.notificationSystem.show(
'All tasks cleared successfully',
'success',
null,
null,
null,
customIcon
);
const msg = skipped.length > 0
? `Deleted ${targets.length} tasks (skipped ${skipped.length} still running)`
: (targets.length === 1 ? 'Task deleted' : `Deleted ${targets.length} tasks`);
window.notificationSystem.show(msg, 'success', null, null, null, customIcon);
}
} catch (error) {
console.error('Error clearing tasks:', error);
// Remove progress notification and show error
if (progressNotification && progressNotification.remove) {
progressNotification.remove();
}