feat(tools): one small spinner toast per tool run
Running a tool from the Tools tab raised two full-size toasts around a few seconds of work — "task started!" as the run began and "task completed!" as it ended — and then opened the result modal that actually carried the answer. The started one was stale by the time it was read and the finished one said what the modal was already showing. Tool tasks now go through LP_BACKGROUND_TASKS with a new `silent` flag (no started toast, no finish line), and tools-manager raises a compact "Running <tool>…" spinner toast for the duration instead, dismissed the moment the result modal or the user list opens. A failed list_users now falls through to the result modal too — it has no account list to open, and the completion toast that used to report the failure is gone.
This commit is contained in:
parent
f8d7dd139d
commit
c26b7190c5
@ -828,19 +828,22 @@ class ToolsManager {
|
||||
}
|
||||
|
||||
// Wait for one specific tool task to finish, then surface its outcome
|
||||
// without leaving the Tools tab.
|
||||
// without leaving the Tools tab. `pending` is the small spinner toast raised
|
||||
// by _dispatch; it comes down the moment we have a result to show.
|
||||
//
|
||||
// list_users is left alone: _maybeOpenUserListModal already listens for the
|
||||
// same event and opens the interactive account list, which is a better
|
||||
// result view than a summary could be.
|
||||
_watchToolTask(tool, taskId) {
|
||||
if (!taskId) return;
|
||||
// list_users is left alone on success: _maybeOpenUserListModal already listens
|
||||
// for the same event and opens the interactive account list, which is a better
|
||||
// result view than a summary could be. A failed run has no account list to
|
||||
// open, so it falls through to the normal result modal like every other tool.
|
||||
_watchToolTask(tool, taskId, pending) {
|
||||
if (!taskId) { pending?.dismiss(); return; }
|
||||
const onDone = (ev) => {
|
||||
const d = ev?.detail || {};
|
||||
const id = d.taskId || d.id || d.task?.id;
|
||||
if (id !== taskId) return; // not ours — keep listening
|
||||
window.removeEventListener('taskCompleted', onDone);
|
||||
if (tool.id === 'list_users') return;
|
||||
pending?.dismiss();
|
||||
if (tool.id === 'list_users' && d.status === 'completed') return;
|
||||
this._showToolResult(tool, taskId, d.status);
|
||||
};
|
||||
window.addEventListener('taskCompleted', onDone);
|
||||
@ -910,6 +913,15 @@ class ToolsManager {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// One small spinner line for the whole run, in place of the standard
|
||||
// "task started!" / "task completed!" pair (suppressed for the `tool`
|
||||
// action in LP_BACKGROUND_TASKS). A tool is a few seconds of work whose
|
||||
// answer arrives in a modal — two full-size toasts around that read as
|
||||
// ceremony, and the started one is already stale by the time it's read.
|
||||
const pending = window.notificationSystem
|
||||
? window.notificationSystem.showPending(`Running ${escapeHtml(tool.label || tool.id)}…`)
|
||||
: null;
|
||||
|
||||
try {
|
||||
const task = await window.tasksManager.router.routeAction('tool', {
|
||||
appName: this.currentApp,
|
||||
@ -929,9 +941,10 @@ class ToolsManager {
|
||||
//
|
||||
// So: stay put, and bring the result to them. The log is still one click
|
||||
// away from the completion notice for anything that needs the detail.
|
||||
this._watchToolTask(tool, task && task.id);
|
||||
this._watchToolTask(tool, task && task.id, pending);
|
||||
} catch (err) {
|
||||
console.error('Tool dispatch failed', err);
|
||||
pending?.dismiss();
|
||||
if (window.notificationSystem) {
|
||||
window.notificationSystem.error(`Tool failed: ${err.message}`);
|
||||
}
|
||||
|
||||
@ -420,8 +420,10 @@ class TasksManager {
|
||||
const bg = window.LP_BACKGROUND_TASKS;
|
||||
if (bg && bg.match(task.type, task.command)) {
|
||||
const handLaunched = bg.pending.delete(String(task.id));
|
||||
if (handLaunched && (task.status === 'completed' || task.status === 'failed')) {
|
||||
const m = bg.messages(task.type);
|
||||
const m = bg.messages(task.type);
|
||||
// `silent` actions (app tools) report their own outcome in a modal —
|
||||
// even the one-line finish toast would be saying it twice.
|
||||
if (!m.silent && handLaunched && (task.status === 'completed' || task.status === 'failed')) {
|
||||
window.notificationSystem.show(
|
||||
task.status === 'completed' ? m.done : m.fail,
|
||||
task.status === 'completed' ? 'success' : 'error'
|
||||
|
||||
@ -36,6 +36,41 @@ class NotificationSystem {
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small "working on it" toast for short actions that report their own
|
||||
* result (e.g. app tools, which end in a result modal).
|
||||
*
|
||||
* Deliberately unlike show(): compact, spinner instead of a status icon, no
|
||||
* app icon or action button, never written to localStorage (a pending state
|
||||
* is meaningless after a reload) and no 10s auto-dismiss — the caller closes
|
||||
* it when the work finishes. A long safety timeout still clears it if the
|
||||
* caller never calls back (processor restart, tab left open).
|
||||
*
|
||||
* Returns a handle: { element, dismiss() }. dismiss() is idempotent.
|
||||
*/
|
||||
showPending(message, options = {}) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'notification notification-pending';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-icon"><span class="notification-spinner" aria-hidden="true"></span></div>
|
||||
<div class="notification-message">${message}</div>
|
||||
</div>`;
|
||||
this.addNotificationToContainer(notification);
|
||||
|
||||
let closed = false;
|
||||
const dismiss = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearTimeout(safety);
|
||||
notification.classList.add('notification-hide');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
};
|
||||
const safety = setTimeout(dismiss, options.timeout || 5 * 60 * 1000);
|
||||
|
||||
return { element: notification, dismiss };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification element with consistent structure
|
||||
*/
|
||||
|
||||
@ -533,9 +533,14 @@ window.TaskActions = TaskActions;
|
||||
// Classified by task action/type; the command regex also catches backend
|
||||
// variants (e.g. `libreportal updater check auto`). Add a `byAction` entry to
|
||||
// make another self-surfacing action quiet — the mechanism is generic.
|
||||
// A `silent: true` entry goes one step further: no started toast and no finish
|
||||
// line at all, for actions that surface their own result in place (app tools
|
||||
// open a result modal). Those show a small "…" pending toast of their own while
|
||||
// the task runs — see tools-manager.js.
|
||||
window.LP_BACKGROUND_TASKS = window.LP_BACKGROUND_TASKS || {
|
||||
byAction: {
|
||||
updater_check: { done: 'Apps checked for updates & vulnerabilities.', fail: 'Update check failed.' },
|
||||
tool: { silent: true },
|
||||
},
|
||||
commandRe: /^libreportal\s+updater\s+check\b/,
|
||||
pending: new Set(), // ids of hand-launched background tasks awaiting a quiet finish toast
|
||||
|
||||
@ -2810,6 +2810,32 @@ html[data-theme="nebula"]::after {
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
||||
/* Compact "working on it" toast (notificationSystem.showPending) — sized to
|
||||
its text rather than the 350px+ status toasts, since it carries one short
|
||||
line and no action button. */
|
||||
.notification-pending {
|
||||
min-width: 0;
|
||||
width: fit-content;
|
||||
max-width: min(420px, calc(100vw - 40px));
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.notification-pending .notification-message {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, var(--text-muted));
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.notification-spinner {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(var(--text-rgb), 0.15);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
/* Mobile positioning */
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user