// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base. Object.assign(TasksManager.prototype, { async retryTask(taskId) { if (!confirm('Are you sure you want to retry this task?')) return; try { const task = this.tasks.find(t => t.id === taskId); if (!task) { throw new Error('Task not found'); } // Create a new task with the same command const newTask = await this.taskManager.createTask( task.command, task.type, task.app, task.config ); //// // console.log(`✅ Task retried: ${newTask.id}`); // Refresh tasks to show the new one await this.loadTasks(); if (window.notificationSystem) { // Use the source task's type icon — retrying a backup shows 💾 etc. const typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon(task)?.icon : ''; const customIcon = typeIcon ? `${typeIcon}` : null; window.notificationSystem.show('Task retried successfully', 'success', null, null, null, customIcon); } } catch (error) { console.error('Error retrying task:', error); if (window.notificationSystem) { window.notificationSystem.error(`Failed to retry task: ${error.message}`); } } }, async deleteTask(taskId) { // Get the latest known status before deciding what to do. The server // refuses to delete tasks that are still running or queued (HTTP 409), // and we used to just propagate that as a red banner — but the user is // almost always trying to clean up a row that looked finished, or a // genuinely active task they want gone. Either way the right answer is // "cancel first, then delete", which is what we do here. const cached = (window.taskEventBus && window.taskEventBus.getTask) ? window.taskEventBus.getTask(taskId) : null; let task = cached || (this.tasks && this.tasks.find(t => t.id === taskId)); if (!task && this.taskManager && this.taskManager.getTask) { try { task = await this.taskManager.getTask(taskId); } catch {} } const isActive = task && (task.status === 'running' || task.status === 'queued' || task.status === 'pending'); const confirmed = await this._showDeleteTaskModal(task, isActive); if (!confirmed) return; try { if (isActive) { // Ask the server to cancel. POST /cancel either flips status straight // to `cancelled` (queued -> cancelled is synchronous) or drops a // `.cancel` marker the bash processor picks up within ~1s // (running -> cancelled). We then wait for the terminal status via // SSE before issuing the delete. try { await this.taskManager.cancelTask(taskId); } catch { // If cancel itself failed (e.g. already terminal), fall through // and try the delete anyway. } await this._waitForTaskTerminal(taskId, 15_000); } // Try the polite delete first. If the task is still flagged as // running/queued server-side (e.g. the bash processor is dead and // never picked up the cancel marker), fall back to the force-delete // override so the user isn't stuck with a permanently-undeletable row. try { await this.taskManager.deleteTask(taskId); } catch (err) { const looks409 = /\bHTTP 409\b/.test(err && err.message ? err.message : ''); if (!looks409) throw err; await this.taskManager.deleteTask(taskId, { force: true }); } // Remove from local array and re-render this.tasks = this.tasks.filter(t => t.id !== taskId); this.renderTasks(); this.updateStats(); this.updateSidebarCounts(); this.generateAppCategories(); if (window.notificationSystem) { // Match the " task " two-line shape used by started/ // completed/failed/cancelled so deletion reads as part of the same // family. Type emoji falls back to 🗑️ since deletion is a 🗑️ action. const { appName, actionTitle, displayName, icon, typeIcon } = this._taskNotificationDescriptor(task); const emoji = typeIcon || '🗑️'; const customIcon = `${emoji}`; const body = displayName ? `${displayName}
${actionTitle} task deleted.` : `${actionTitle || 'Task'} deleted.`; window.notificationSystem.show(body, 'info', appName, null, icon, customIcon); } } catch (error) { console.error('Error deleting task:', error); if (window.notificationSystem) { window.notificationSystem.error(`Failed to delete task: ${error.message}`); } } }, async clearAllTasks() { // Routes to one of two modes depending on whether any rows are ticked. // Both paths share _showClearAllModal — same UX, same modal, different // input list + title. const hasSelection = this.selectedTaskIds.size > 0; const targetTasks = hasSelection ? this.tasks.filter(t => this.selectedTaskIds.has(t.id)) : this.tasks; const result = await this._showClearAllModal(targetTasks, hasSelection ? 'selected' : 'all'); if (!result || !result.confirmed) return false; await this.performClearAll({ cancelRunning: result.cancelRunning, targets: targetTasks }); if (hasSelection) { // Drop any ids we just deleted from the selection set, then refresh // the button label + master-checkbox state. this.selectedTaskIds = new Set([...this.selectedTaskIds].filter(id => this.tasks.find(t => t.id === id))); this._updateSelectionUI(); } return true; }, async performClearAll(opts) { opts = opts || {}; const cancelRunning = !!opts.cancelRunning; // Caller (clearAllTasks) passes the subset to operate on. Falls back // to this.tasks for backward compatibility — old direct callers // continue to mean "everything". const universe = Array.isArray(opts.targets) ? opts.targets : (this.tasks || []); // 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 = '🗑️'; const progressNotification = window.notificationSystem.show( 'Clearing tasks...', 'info', null, null, null, 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 = universe.filter(t => cancelRunning || !isActive(t)); const skipped = universe.filter(t => !targets.includes(t)); try { // 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 }); } })); // 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(); if (progressNotification && progressNotification.remove) { progressNotification.remove(); } if (window.notificationSystem) { 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); if (progressNotification && progressNotification.remove) { progressNotification.remove(); } if (window.notificationSystem) { window.notificationSystem.show( `Failed to clear tasks: ${error.message}`, 'error', null, null, null, customIcon ); } } }, // Selection helpers wired by the row + master checkboxes. toggleTaskSelection(taskId, checked) { if (checked) this.selectedTaskIds.add(taskId); else this.selectedTaskIds.delete(taskId); this._updateSelectionUI(); }, toggleSelectAll(checked) { if (checked) { // Tick every CURRENTLY VISIBLE row. We use the rendered checkboxes // rather than this.tasks so category-filtered views only select // what the user can see. const boxes = document.querySelectorAll('#tasks-list [data-task-select]'); boxes.forEach((cb) => { this.selectedTaskIds.add(cb.dataset.taskSelect); cb.checked = true; }); } else { this.selectedTaskIds.clear(); document.querySelectorAll('#tasks-list [data-task-select]').forEach((cb) => { cb.checked = false; }); } this._updateSelectionUI(); }, // Rewrites the Clear All button label + the master-checkbox indeterminate // state to reflect the current selection. Cheap — only touches a few // DOM nodes, safe to call from any selection-change path. _updateSelectionUI() { const n = this.selectedTaskIds.size; const btn = document.getElementById('tasks-clear-btn'); const btnLabel = btn && btn.querySelector('.clear-btn-label'); if (btnLabel) btnLabel.textContent = n > 0 ? `Delete Selected (${n})` : 'Clear All'; if (btn) btn.title = n > 0 ? `Delete ${n} selected task${n === 1 ? '' : 's'}` : 'Clear All Tasks'; // Master checkbox: checked when ALL visible are picked, indeterminate // when SOME are, unchecked when none. const master = document.getElementById('tasks-select-all'); const visible = document.querySelectorAll('#tasks-list [data-task-select]'); if (master) { if (n === 0 || visible.length === 0) { master.checked = false; master.indeterminate = false; } else if (n >= visible.length) { master.checked = true; master.indeterminate = false; } else { master.checked = false; master.indeterminate = true; } } }, // Resolves once the task reaches completed/failed/cancelled, or after the // timeout. Used by deleteTask so a cancel request has time to take effect // before we issue the DELETE that would otherwise 409. _waitForTaskTerminal(taskId, timeoutMs = 15_000) { return new Promise((resolve) => { // Already terminal in the bus cache? No need to wait. const cached = (window.taskEventBus && window.taskEventBus.getTask) ? window.taskEventBus.getTask(taskId) : null; if (cached && (cached.status === 'completed' || cached.status === 'failed' || cached.status === 'cancelled')) { return resolve(cached); } let timer; const cleanup = () => { window.removeEventListener('taskCompleted', onComplete); window.removeEventListener('taskUpdated', onUpdate); if (timer) clearTimeout(timer); }; const isTerminal = (s) => s === 'completed' || s === 'failed' || s === 'cancelled'; const onComplete = (e) => { const t = e.detail && (e.detail.task || (e.detail.taskId === taskId ? { id: taskId, status: e.detail.status } : null)); const id = (t && t.id) || (e.detail && e.detail.taskId); if (id !== taskId) return; cleanup(); resolve(t); }; const onUpdate = (e) => { const t = e.detail && e.detail.task; if (!t || t.id !== taskId) return; if (isTerminal(t.status)) { cleanup(); resolve(t); } }; window.addEventListener('taskCompleted', onComplete); window.addEventListener('taskUpdated', onUpdate); timer = setTimeout(() => { cleanup(); resolve(null); }, timeoutMs); }); }, });