🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Background Job Toasts

by @maverick-software

Background job toast notification system for the OpenClaw Control UI. Bottom-right corner toast bar that shows running/complete/error status for all backgrou...

Versionv1.0.0
Downloads462
TERMINAL
clawhub install bg-job-toasts

πŸ“– About This Skill


name: bg-job-toasts description: "Background job toast notification system for the OpenClaw Control UI. Bottom-right corner toast bar that shows running/complete/error status for all background processes β€” cron jobs, memory compaction, knowledge extraction, and any custom jobs. Includes gateway cron event enrichment (job name forwarded in broadcast payload) and context gauge button with live spinner + progress modal. Use when adding or improving background job visibility in the OpenClaw UI, fixing 'Background job' fallback names on toasts, or troubleshooting the compaction button showing no progress."

Background Job Toasts β€” OpenClaw Control UI

Real-time bottom-right toast bar that shows running / complete / error status for all background processes in the OpenClaw Control UI: cron jobs, memory compaction, knowledge extraction, and any consumer that pushes to backgroundJobToasts.

Status: βœ… Active

| Component | Status | |-----------|--------| | Toast bar (bottom-right) | βœ… Working | | Cron job name enrichment (gateway) | βœ… Working | | Context gauge spinner during compaction | βœ… Working | | Compaction modal phase labels + progress bar | βœ… Working | | Auto-dismiss after completion | βœ… Working |

Architecture

Data Type (ui/src/ui/app-view-state.ts)

export type BackgroundJobToast = {
  jobId: string;        // Unique identifier (use "cmp-" prefix for compaction)
  jobName: string;      // Human-readable label shown in the toast
  status: "running" | "complete" | "error";
  startedAt: number;    // Date.now()
  completedAt: number | null;
  errorMsg?: string;
};

backgroundJobToasts: BackgroundJobToast[] is a LitElement @state() property on OpenClawApp (app.ts). Assigning to it directly triggers an automatic re-render β€” no requestUpdate() needed for the toast array itself.

Toast Rendering (ui/src/ui/app-render.ts)

renderBackgroundJobToasts(state) renders the .bg-job-toasts container at the bottom of the app shell (called in the root render() function). It maps each toast to a .bg-job-toast--{status} div with an icon, name, and label.

Status indicators:

  • running β†’ spinner icon (CSS animation compaction-spin), blue tint, "running..."
  • complete β†’ checkmark icon, green tint, "done"
  • error β†’ X icon, red tint, "failed"
  • Styles (ui/src/styles/components.css)

    .bg-job-toasts   { position: fixed; bottom: 24px; right: 24px; z-index: 9000; … }
    .bg-job-toast    { border-radius: var(--radius-lg); background: var(--panel-strong); … }
    .bg-job-toast--running  { color: var(--info); … }
    .bg-job-toast--complete { color: var(--ok); … }
    .bg-job-toast--error    { color: var(--danger); … }
    

    Entry animation: bg-job-toast-in (0.2s, slide up from 6px).

    Gateway Cron Event Enrichment (src/gateway/server-cron.ts)

    The gateway's onEvent callback enriches cron events with the job name before broadcasting, so the UI can display a meaningful label even when the Cron tab hasn't been opened yet (and thus cronJobs isn't loaded):

    onEvent: (evt) => {
      const jobForName = cron.getJob(evt.jobId);
      const enriched = jobForName?.name ? { ...evt, name: jobForName.name } : evt;
      params.broadcast("cron", enriched, { dropIfSlow: true });
      // ... rest of webhook handling
    }
    

    UI Cron Event Handler (ui/src/ui/app-gateway.ts)

    const cronPayload = evt.payload as {
      jobId?: string;
      name?: string;       // ← enriched by gateway
      action?: string;
      status?: string;
      error?: string;
    };
    // Priority: event.name > loaded cronJobs list > fallback
    const jobName = cronPayload.name ?? matchedJob?.name ?? "Background job";
    

    Context Gauge + Compaction Button (ui/src/ui/app-render.helpers.ts)

    The renderContextGauge(state) function renders a circular SVG ring in the chat toolbar. When compaction is running:

  • The gauge ring is replaced by a spinning ⟳ icon
  • A compactState.phase overlay modal appears with live status
  • Phase variables must be defined before the template:

    const phaseLabel =
      compactState.phase === "running"
        ? "Summarizing your conversation…"
        : compactState.phase === "complete"
          ? "Done! Context has been reduced."
          : compactState.phase === "error"
            ? "Something went wrong."
            : "";
    const isWorking = compactState.phase === "running";
    

    compactState is read from (state as unknown as Record).__compactState β€” a plain property (not @state()), so app.requestUpdate() must be called explicitly after mutations.

    Compaction flow: 1. Button click β†’ __compactState = { active: true, phase: "running" } + push "running" toast β†’ requestUpdate() 2. RPC sessions.compact resolves β†’ update toast to "complete" with token counts β†’ __compactState = {} β†’ requestUpdate() 3. Auto-dismiss toast after 5 seconds

    Files Modified

    | File | Change | |------|--------| | src/gateway/server-cron.ts | Enrich cron event with name before broadcasting | | ui/src/ui/app-gateway.ts | Read cronPayload.name first in job name resolution | | ui/src/ui/app-render.helpers.ts | Define phaseLabel/isWorking; spinner on gauge button while running |

    How to Push a Custom Toast from Anywhere

    Any code with access to the OpenClawApp instance can push a toast:

    const app = state as unknown as OpenClawApp;
    const jobId = "my-job-" + Date.now();

    // Start app.backgroundJobToasts = [ ...(app.backgroundJobToasts ?? []).filter(t => t.jobId !== jobId), { jobId, jobName: "My Task", status: "running", startedAt: Date.now(), completedAt: null }, ];

    // Complete (with auto-dismiss) app.backgroundJobToasts = [ ...(app.backgroundJobToasts ?? []).filter(t => t.jobId !== jobId), { jobId, jobName: "My Task done", status: "complete", startedAt: Date.now(), completedAt: Date.now() }, ]; window.setTimeout(() => { app.backgroundJobToasts = (app.backgroundJobToasts ?? []).filter(t => t.jobId !== jobId); }, 5000);

    > Prefix convention: Use "cmp-" for compaction jobs (filtered when a new compaction starts). Use a unique domain prefix for other job types.

    Known Gotchas

  • phaseLabel/isWorking must be defined before the template literal β€” referencing them without defining them compiles fine (TypeScript doesn't catch undeclared variables inside template literals) but renders as undefined at runtime, producing a blank modal with no text and no progress bar.
  • __compactState is not @state() β€” always call app.requestUpdate() after mutating it, or the UI won't re-render.
  • Cron job name fallback order matters β€” the name field in the event payload (gateway-enriched) must be checked before the local cronJobs list, because the list is only loaded when the Cron tab is open.
  • backgroundJobToasts IS @state() β€” assigning the array triggers a re-render automatically; no requestUpdate() needed specifically for that.
  • Reference

    See references/source-snapshot.md for a snapshot of the key code sections at time of writing.