feat(04-01): refactor all notify() call sites to use structured NotificationOptions
- Change ipc-support.ts notify() from (title, body) to (options: NotificationOptions) - Wire createNotificationService in bootstrap-application.ts with lazy service graph refs - Update all 12 call sites with entity context, urgency, navigation, and batch keys - Update dependency interfaces in service-graph, worker, orchestration, automation, updater, desktop-shell - No string-based notify() calls remain in src/main/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9ebb489644
commit
667ac48aa5
8 files changed
+113
-34
No files matched your search
@@ -8,6 +8,7 @@ import type {
|
||||
RunStatus,
|
||||
} from "@shared/types";
|
||||
import { dispatchAgentMessage } from "./message-dispatch";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
const WORKFLOW_ACTIONS: ReadonlySet<AutomationAction> = new Set([
|
||||
"assign_task",
|
||||
@@ -59,7 +60,7 @@ interface AutomationDependencies {
|
||||
};
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
publishDomainChanged: () => void;
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
wakeAgentIfPossible: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
|
||||
dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void>;
|
||||
}
|
||||
@@ -909,16 +910,21 @@ export function createAutomationService({
|
||||
});
|
||||
// Fire OS notification for budget threshold warning
|
||||
const pct = Math.round(utilization * 100);
|
||||
notify(
|
||||
"Budget Alert",
|
||||
`${agent.name} is at ${pct}% of monthly budget ($${agent.spentMonthlyUsd.toFixed(2)} / $${agent.budgetMonthlyUsd.toFixed(2)})`,
|
||||
);
|
||||
notify({
|
||||
title: "Budget Warning",
|
||||
body: `${agent.name} has used ${pct}% of monthly budget`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "costs" },
|
||||
batchKey: "budget_alerts",
|
||||
});
|
||||
if (utilization >= 1.0) {
|
||||
// Fire OS notification for budget exhaustion (agent auto-paused by database)
|
||||
notify(
|
||||
"Budget Alert",
|
||||
`${agent.name} paused -- monthly budget exhausted ($${agent.spentMonthlyUsd.toFixed(2)} / $${agent.budgetMonthlyUsd.toFixed(2)})`,
|
||||
);
|
||||
notify({
|
||||
title: "Budget Exhausted",
|
||||
body: `${agent.name} has reached 100% of monthly budget and is paused`,
|
||||
urgency: "critical",
|
||||
navigation: { section: "costs" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -957,7 +963,13 @@ export function createAutomationService({
|
||||
});
|
||||
wakeAgentIfPossible(agent.id, companyId, "assignment");
|
||||
publishDomainChanged();
|
||||
notify("New agent hired", `${agent.name} (${agent.role}) has joined the company.`);
|
||||
notify({
|
||||
title: "Agent Hired",
|
||||
body: `${agent.name} joined as ${agent.role}`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "orgchart", entityId: agent.id },
|
||||
batchKey: "agent_hired",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Hire follow-up is best-effort; approval state is already committed in storage.
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { BrowserManager } from "./browser-manager";
|
||||
import { prepareHostEnvironment } from "./host-environment-service";
|
||||
import { createServiceGraph } from "./service-graph";
|
||||
import { createIpcSupport } from "./ipc-support";
|
||||
import { createNotificationService } from "./notification-service";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
|
||||
interface BootstrapApplicationDependencies {
|
||||
@@ -73,11 +75,19 @@ export async function bootstrapApplication({
|
||||
const apiServerRef = { current: null as AgentApiServer | null };
|
||||
const browserManagerRef = { current: null as BrowserManager | null };
|
||||
let ipcSupport: ReturnType<typeof createIpcSupport> | null = null;
|
||||
const notify = (title: string, body: string) => {
|
||||
let serviceGraph: ReturnType<typeof createServiceGraph> | null = null;
|
||||
|
||||
const notificationService = createNotificationService({
|
||||
getMainWindow: () => serviceGraph?.getDesktopShellService()?.getMainWindow() ?? null,
|
||||
emitEvent: (event: DesktopEvent) => ipcSupport?.emitEvent(event),
|
||||
logger: log,
|
||||
});
|
||||
|
||||
const notify = (options: NotificationOptions) => {
|
||||
if (!ipcSupport) {
|
||||
throw new Error("IPC support is not initialized.");
|
||||
}
|
||||
ipcSupport.notify(title, body);
|
||||
ipcSupport.notify(options);
|
||||
};
|
||||
const emitEvent = (event: DesktopEvent) => {
|
||||
ipcSupport?.emitEvent(event);
|
||||
@@ -86,7 +96,7 @@ export async function bootstrapApplication({
|
||||
ipcSupport?.publishDomainChanged();
|
||||
};
|
||||
|
||||
const serviceGraph = createServiceGraph({
|
||||
serviceGraph = createServiceGraph({
|
||||
db,
|
||||
apiServerRef,
|
||||
browserManagerRef,
|
||||
@@ -109,11 +119,12 @@ export async function bootstrapApplication({
|
||||
isDev,
|
||||
devServerOrigin,
|
||||
logger: log,
|
||||
getMainWindow: () => serviceGraph.getDesktopShellService()?.getMainWindow() ?? null,
|
||||
getMainWindow: () => serviceGraph?.getDesktopShellService()?.getMainWindow() ?? null,
|
||||
getDb: () => ({
|
||||
hasCompany: (companyId: string) => db.hasCompany(companyId),
|
||||
belongsToCompany: (table: string, id: string, companyId: string) => db.belongsToCompany(table as any, id, companyId),
|
||||
}),
|
||||
notificationService,
|
||||
});
|
||||
serviceGraph.init();
|
||||
|
||||
@@ -242,10 +253,12 @@ export async function bootstrapApplication({
|
||||
type: "recovery-needed",
|
||||
interruptedRuns,
|
||||
} as any); // DesktopEvent will need extending
|
||||
notify(
|
||||
"Interrupted runs detected",
|
||||
`${interruptedRuns.length} task(s) were interrupted when the app last closed. Check the dashboard for recovery options.`
|
||||
);
|
||||
notify({
|
||||
title: "Session Recovered",
|
||||
body: `${interruptedRuns.length} interrupted task(s) detected from previous session`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "overview" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Tray,
|
||||
} from "electron";
|
||||
import { attachWindowStatePersistence, loadWindowState } from "./window-state";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
interface DesktopShellDependencies {
|
||||
preloadPath: string;
|
||||
@@ -28,7 +29,7 @@ interface DesktopShellDependencies {
|
||||
onCheckForUpdates: () => Promise<void>;
|
||||
onBackupProfile: () => Promise<void>;
|
||||
onShortcut: (action: "toggle-command-palette" | "toggle-console" | "refresh") => void;
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
}
|
||||
|
||||
function isSocialBrowserSession(contents: Electron.WebContents) {
|
||||
@@ -155,7 +156,12 @@ export function createDesktopShellService({
|
||||
mainWindow?.hide();
|
||||
if (!backgroundCloseNoticeShown) {
|
||||
backgroundCloseNoticeShown = true;
|
||||
notify("AgentCompany is still running", "Automation continues in the background until you quit the app.");
|
||||
notify({
|
||||
title: "Running in Background",
|
||||
body: "AgentCompany is still running in the system tray",
|
||||
urgency: "informational",
|
||||
navigation: { section: "overview" },
|
||||
});
|
||||
}
|
||||
});
|
||||
mainWindow.on("closed", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ipcMain, Notification } from "electron";
|
||||
import { ZodError } from "zod";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import type { DesktopResult } from "@shared/contracts";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
interface IpcSupportDependencies {
|
||||
isDev: boolean;
|
||||
@@ -15,6 +16,7 @@ interface IpcSupportDependencies {
|
||||
hasCompany: (companyId: string) => boolean;
|
||||
belongsToCompany: (table: string, id: string, companyId: string) => boolean;
|
||||
};
|
||||
notificationService?: { notify: (options: NotificationOptions) => void };
|
||||
}
|
||||
|
||||
function isTrustedRendererUrl(url: string, isDev: boolean, devServerOrigin: string | null) {
|
||||
@@ -39,6 +41,7 @@ export function createIpcSupport({
|
||||
logger,
|
||||
getMainWindow,
|
||||
getDb,
|
||||
notificationService,
|
||||
}: IpcSupportDependencies) {
|
||||
function sanitizeHandlerError(error: unknown) {
|
||||
if (error instanceof ZodError) {
|
||||
@@ -102,9 +105,12 @@ export function createIpcSupport({
|
||||
emitEvent({ type: "domain-changed" });
|
||||
}
|
||||
|
||||
function notify(title: string, body: string) {
|
||||
if (Notification.isSupported()) {
|
||||
new Notification({ title, body }).show();
|
||||
function notify(options: NotificationOptions) {
|
||||
if (notificationService) {
|
||||
notificationService.notify(options);
|
||||
} else if (Notification.isSupported()) {
|
||||
// Fallback for early bootstrap before notification service is wired
|
||||
new Notification({ title: options.title, body: options.body }).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AppDatabase } from "./database";
|
||||
import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy";
|
||||
import type { DesktopResult } from "@shared/contracts";
|
||||
import type { ConnectorRecord } from "@shared/types";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
type WorkspaceLockMode = "exclusive" | "shared" | "none";
|
||||
|
||||
@@ -53,7 +54,7 @@ interface OrchestrationDependencies {
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
publishDomainChanged: () => void;
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
createRunToken: (agentId: string, companyId: string, runId: string) => string;
|
||||
getApiPort: () => number;
|
||||
dispatchAutomation: (trigger: string, ctx: AutomationDispatch) => void;
|
||||
@@ -128,7 +129,13 @@ export function createOrchestrationService({
|
||||
entityId: agent.id,
|
||||
detail: `Budget exhausted at ${agent.spentMonthlyUsd.toFixed(2)} / ${agent.budgetMonthlyUsd.toFixed(2)} USD.`,
|
||||
});
|
||||
notify("Agent paused for budget limit", agent.name);
|
||||
notify({
|
||||
title: "Agent Paused",
|
||||
body: `${agent.name} paused -- budget limit reached`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "agents", entityId: agent.id },
|
||||
batchKey: "agent_paused",
|
||||
});
|
||||
publishDomainChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createWorkerService } from "./worker-service";
|
||||
import { createAppLifecycleService } from "./app-lifecycle-service";
|
||||
import type { DesktopEvent, RunRecord } from "@shared/types";
|
||||
import type { AutomationEventContext } from "./automation-service";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
interface ServiceGraphDependencies {
|
||||
db: AppDatabase;
|
||||
@@ -32,7 +33,7 @@ interface ServiceGraphDependencies {
|
||||
};
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
publishDomainChanged: () => void;
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
createRunToken: (agentId: string, companyId: string, runId: string) => string;
|
||||
seedDefaultAutomationRules: (companyId: string) => void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import electronUpdater from "electron-updater";
|
||||
import type { AppDatabase } from "./database";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
const { autoUpdater } = electronUpdater;
|
||||
|
||||
@@ -9,7 +10,7 @@ interface UpdaterServiceDependencies {
|
||||
info: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
}
|
||||
|
||||
export function createUpdaterService({
|
||||
@@ -22,14 +23,29 @@ export function createUpdaterService({
|
||||
logger.info("[updater] checking for updates");
|
||||
});
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
notify("Update available", `AgentCompany ${info.version} is available to download.`);
|
||||
notify({
|
||||
title: "Update Available",
|
||||
body: `Version ${info.version} is ready to install`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "settings" },
|
||||
});
|
||||
});
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
notify("No updates found", "AgentCompany is already on the latest configured version.");
|
||||
notify({
|
||||
title: "Up to Date",
|
||||
body: "You are running the latest version",
|
||||
urgency: "informational",
|
||||
navigation: { section: "settings" },
|
||||
});
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
logger.error("[updater] error", error);
|
||||
notify("Update check failed", error instanceof Error ? error.message : String(error));
|
||||
notify({
|
||||
title: "Update Check Failed",
|
||||
body: "Could not check for updates. Try again later.",
|
||||
urgency: "informational",
|
||||
navigation: { section: "settings" },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { getConnectorDefinition } from "./connectors";
|
||||
import type { AppDatabase } from "./database";
|
||||
import type { RunRecord } from "@shared/types";
|
||||
import type { NotificationOptions } from "./notification-service";
|
||||
|
||||
const runStatusSchema = z.enum(["queued", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"]);
|
||||
export const workerMessageSchema = z.union([
|
||||
@@ -61,7 +62,7 @@ interface WorkerServiceDependencies {
|
||||
trigger: string;
|
||||
}) => void;
|
||||
processRunCompletion: (run: RunRecord, event: z.infer<typeof workerMessageSchema> & { type: "run-finished" }) => void;
|
||||
notify: (title: string, body: string) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
}
|
||||
|
||||
export function createWorkerService({
|
||||
@@ -284,7 +285,13 @@ export function createWorkerService({
|
||||
message: event.summary ?? event.errorMessage ?? event.status,
|
||||
});
|
||||
if ((event.status === "failed" || event.status === "timed_out" || event.status === "interrupted") && task) {
|
||||
notify("Run attention required", task.title);
|
||||
notify({
|
||||
title: "Task Failed",
|
||||
body: `${task.title} -- manual intervention needed`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "tasks", entityId: task.id },
|
||||
batchKey: "failed_runs",
|
||||
});
|
||||
}
|
||||
|
||||
let updatedAgent: ReturnType<AppDatabase["getAgent"]> | null = null;
|
||||
@@ -303,7 +310,13 @@ export function createWorkerService({
|
||||
entityId: updatedAgent.id,
|
||||
detail: `Budget exhausted at ${updatedAgent.spentMonthlyUsd.toFixed(2)} / ${updatedAgent.budgetMonthlyUsd.toFixed(2)} USD.`,
|
||||
});
|
||||
notify("Agent paused for budget limit", updatedAgent.name);
|
||||
notify({
|
||||
title: "Agent Paused",
|
||||
body: `${updatedAgent.name} paused -- budget limit reached`,
|
||||
urgency: "informational",
|
||||
navigation: { section: "agents", entityId: updatedAgent.id },
|
||||
batchKey: "agent_paused",
|
||||
});
|
||||
publishDomainChanged();
|
||||
}
|
||||
|
||||
@@ -351,7 +364,12 @@ export function createWorkerService({
|
||||
if (!escalated) {
|
||||
// No one to escalate to -- notify user
|
||||
const taskTitle = task?.title ?? run.taskId;
|
||||
notify("Task failed -- manual intervention needed", `"${taskTitle}" failed ${MAX_RETRY_ATTEMPTS} times and no manager is available to handle it.`);
|
||||
notify({
|
||||
title: "Attention Required",
|
||||
body: `${taskTitle} needs manual intervention`,
|
||||
urgency: "critical",
|
||||
navigation: { section: "tasks", entityId: run.taskId },
|
||||
});
|
||||
logger.error(`[escalation] Task ${run.taskId} failed with no escalation target. User notified.`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user