mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-26 19:44:51 +02:00
[eric] notifications: a finished workflow's native notification called a preload bridge that was never added, so it never fired
This commit is contained in:
@@ -59,3 +59,66 @@ export function notifyAgentCompletion(p: AgentCompletionPayload): void {
|
||||
// Notification API can throw if sandboxed or headless; fail silently.
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkflowNotificationOutcome = 'open' | 'ack' | 'rerun' | 'edit';
|
||||
|
||||
export interface WorkflowRunNotification {
|
||||
workflowId: string;
|
||||
workflowTitle: string;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
status: string;
|
||||
tierKind?: string;
|
||||
fallback?: boolean;
|
||||
}
|
||||
|
||||
const SUCCESS_TITLES = ['{name} is done', '{name} just wrapped up', 'Heads up: {name} finished', '{name} is ready'];
|
||||
const FAILURE_TITLES = ['{name} hit a snag', "{name} couldn't finish", 'Something went sideways on {name}'];
|
||||
const LATE_TITLES = ['{name} caught up late', '{name} ran late but made it'];
|
||||
|
||||
function workflowTitleFor(p: WorkflowRunNotification): string {
|
||||
const name = p.workflowTitle || 'Workflow';
|
||||
const pool = p.status === 'success' ? SUCCESS_TITLES
|
||||
: p.status === 'failure' ? FAILURE_TITLES
|
||||
: p.status === 'ran_late' ? LATE_TITLES
|
||||
: null;
|
||||
if (!pool) return `${name}: ${p.status}`;
|
||||
// Seed by workflow id + current minute so two workflows pick different copy while one workflow stays stable across a few minutes.
|
||||
const seed = Math.abs((p.workflowId.length + Math.floor(Date.now() / 60_000)) | 0);
|
||||
return pool[seed % pool.length].replace('{name}', name);
|
||||
}
|
||||
|
||||
function workflowBodyFor(p: WorkflowRunNotification): string {
|
||||
if (p.tierKind && p.fallback) {
|
||||
return `Would have ${p.tierKind === 'call' ? 'called' : 'texted'} you. (Cloud SMS not wired yet.)`;
|
||||
}
|
||||
const verb = typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform) ? 'Tap' : 'Click';
|
||||
if (p.status === 'success') return `${verb} to see what it did.`;
|
||||
if (p.status === 'failure') return `${verb} to see what went wrong.`;
|
||||
return `${verb} to open the run.`;
|
||||
}
|
||||
|
||||
/** Native OS notification for a finished workflow run. Prefers the Electron main process, which reaches Notification Center even with the window hidden or the renderer backgrounded; the renderer's own Notification API is the browser-only fallback and it only fires when the tab is hidden. */
|
||||
export function notifyWorkflowRun(p: WorkflowRunNotification): void {
|
||||
const bridge = typeof window !== 'undefined' ? window.openswarm : undefined;
|
||||
if (!bridge?.notify) {
|
||||
notifyAgentCompletion({
|
||||
sessionId: p.sessionId || p.workflowId,
|
||||
sessionName: p.workflowTitle || 'Workflow',
|
||||
status: p.status === 'success' ? 'completed' : 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
bridge.notify({
|
||||
title: workflowTitleFor(p),
|
||||
body: workflowBodyFor(p),
|
||||
deepLink: `openswarm://workflow/${p.workflowId}/run/${p.runId || ''}`,
|
||||
runId: p.runId,
|
||||
workflowId: p.workflowId,
|
||||
actions: [
|
||||
{ text: 'Looks good', outcome: 'ack' },
|
||||
{ text: 'Re-run', outcome: 'rerun' },
|
||||
{ text: 'Adjust', outcome: 'edit' },
|
||||
],
|
||||
}).catch(() => { /* the OS refused it; the run is still on the canvas */ });
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { displaySessionName } from '../state/sessionDisplay';
|
||||
import { upsertRun, ackRun, runWorkflowNow, openWorkflowCard, upsertWorkflow, removeWorkflow } from '../state/workflowsSlice';
|
||||
import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils';
|
||||
import { getAuthToken } from '../config';
|
||||
import { notifyAgentCompletion } from '../notifications';
|
||||
import { notifyAgentCompletion, notifyWorkflowRun } from '../notifications';
|
||||
|
||||
// Phase 0 boot instrumentation: one-shot flag so we report the first streamed agent token to Electron main exactly once per app launch. Module scope (not instance) because multiple WebSocketManagers exist (one per session WS).
|
||||
let firstAgentResponseMarked = false;
|
||||
@@ -763,55 +763,17 @@ class WebSocketManager {
|
||||
break;
|
||||
|
||||
case 'workflow:notify':
|
||||
try {
|
||||
notifyAgentCompletion({
|
||||
sessionId: data.session_id || data.workflow_id,
|
||||
sessionName: data.workflow_title || 'Workflow',
|
||||
status: data.status === 'success' ? 'completed' : 'error',
|
||||
if (data.workflow_id) {
|
||||
notifyWorkflowRun({
|
||||
workflowId: data.workflow_id,
|
||||
workflowTitle: data.workflow_title || 'Workflow',
|
||||
runId: data.run_id,
|
||||
sessionId: data.session_id,
|
||||
status: data.status,
|
||||
tierKind: data.tier_kind,
|
||||
fallback: data.fallback,
|
||||
});
|
||||
} catch { /* notifications are best-effort */ }
|
||||
try {
|
||||
const w: any = (window as any).openswarm;
|
||||
if (w?.notify) {
|
||||
// Seed by workflow id + current minute so multiple workflows pick different copy while a single workflow stays stable within a few minutes.
|
||||
const seed = ((data.workflow_id || '').length + Math.floor(Date.now() / 60000)) | 0;
|
||||
const SUCCESS_TITLES = [
|
||||
`${data.workflow_title || 'Workflow'} — done`,
|
||||
`${data.workflow_title || 'Workflow'} just wrapped up`,
|
||||
`Heads up: ${data.workflow_title || 'Workflow'} finished`,
|
||||
`${data.workflow_title || 'Workflow'} is ready`,
|
||||
];
|
||||
const FAILURE_TITLES = [
|
||||
`${data.workflow_title || 'Workflow'} hit a snag`,
|
||||
`${data.workflow_title || 'Workflow'} couldn't finish`,
|
||||
`Something went sideways on ${data.workflow_title || 'Workflow'}`,
|
||||
];
|
||||
const LATE_TITLES = [
|
||||
`${data.workflow_title || 'Workflow'} caught up late`,
|
||||
`${data.workflow_title || 'Workflow'} ran late but made it`,
|
||||
];
|
||||
const pool = data.status === 'success' ? SUCCESS_TITLES
|
||||
: data.status === 'failure' ? FAILURE_TITLES
|
||||
: data.status === 'ran_late' ? LATE_TITLES
|
||||
: [`${data.workflow_title || 'Workflow'} • ${data.status}`];
|
||||
const title = pool[Math.abs(seed) % pool.length];
|
||||
const isMac = (typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform));
|
||||
const body = data.tier_kind && data.fallback
|
||||
? `Would have ${data.tier_kind === 'call' ? 'called' : 'texted'} you. (Cloud SMS not wired yet.)`
|
||||
: data.status === 'success'
|
||||
? (isMac ? 'Tap to see what it did.' : 'Click to see what it did.')
|
||||
: data.status === 'failure'
|
||||
? (isMac ? 'Tap to see what went wrong.' : 'Click to see what went wrong.')
|
||||
: (isMac ? 'Tap to open the run.' : 'Click to open the run.');
|
||||
const deepLink = data.workflow_id ? `openswarm://workflow/${data.workflow_id}/run/${data.run_id || ''}` : undefined;
|
||||
const actions = [
|
||||
{ text: 'Looks good', outcome: 'ack' },
|
||||
{ text: 'Re-run', outcome: 'rerun' },
|
||||
{ text: 'Adjust', outcome: 'edit' },
|
||||
];
|
||||
w.notify({ title, body, deepLink, runId: data.run_id, workflowId: data.workflow_id, actions });
|
||||
}
|
||||
} catch { /* native notif optional */ }
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dashboard:browser_card_keep':
|
||||
|
||||
Reference in New Issue
Block a user