diff --git a/src/main/api-server.ts b/src/main/api-server.ts index ed934801..9a94f9ab 100644 --- a/src/main/api-server.ts +++ b/src/main/api-server.ts @@ -9,7 +9,7 @@ import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-p import type { AppDatabase } from "./database"; import { inferTaskType } from "./connector-matching"; import { dispatchAgentMessage } from "./message-dispatch"; -import type { DesktopEvent } from "@shared/types"; +import type { DesktopEvent, SectionId } from "@shared/types"; /** * Decode an HTTP request body to a UTF-8 string, handling the common Windows @@ -78,6 +78,7 @@ export interface ApiServerEventCallbacks { onDocumentCreated: (companyId: string, authorAgentId: string | null, projectId: string | null) => void; onAutoAssignTask: (taskId: string, companyId: string) => string | null; emitEvent: (event: DesktopEvent) => void; + notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void; } export class AgentApiServer { @@ -114,6 +115,7 @@ export class AgentApiServer { onDocumentCreated: callbacks?.onDocumentCreated ?? (() => {}), onAutoAssignTask: callbacks?.onAutoAssignTask ?? (() => null), emitEvent: callbacks?.emitEvent ?? (() => {}), + notify: callbacks?.notify ?? (() => {}), }; this.registerRoutes(); } @@ -1009,6 +1011,14 @@ export class AgentApiServer { detail: "Auto-approved by company policy", }); log.info(`[auto-approve] Auto-approved approval ${id}: ${input.payloadSummary}`); + } else { + this.callbacks.notify({ + title: "Approval Required", + body: input.payloadSummary ?? `${input.type} approval pending`, + urgency: "critical", + navigation: { section: "approvals", entityId: id }, + batchKey: "pending_approval", + }); } this.onDomainChanged(); diff --git a/src/main/bootstrap-application.ts b/src/main/bootstrap-application.ts index a92ce720..6c084acd 100644 --- a/src/main/bootstrap-application.ts +++ b/src/main/bootstrap-application.ts @@ -8,7 +8,7 @@ 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"; +import type { DesktopEvent, SectionId } from "@shared/types"; interface BootstrapApplicationDependencies { hasSingleInstanceLock: boolean; @@ -33,6 +33,7 @@ interface BootstrapApplicationDependencies { onDocumentCreated: (companyId: string, authorAgentId: string | null, projectId: string | null) => void; onAutoAssignTask: (taskId: string, companyId: string) => string | null; emitEvent: (event: DesktopEvent) => void; + notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void; }, allowedOrigins: string[]) => AgentApiServer; createRunToken: (agentId: string, companyId: string, runId: string) => string; seedDefaultAutomationRules: (companyId: string) => void; @@ -168,6 +169,7 @@ export async function bootstrapApplication({ }, onAutoAssignTask: (taskId, companyId) => db.autoAssignTask(taskId, companyId), emitEvent, + notify, }, [isDev && devServerUrl ? new URL(devServerUrl).origin : null, "app://-"].filter((origin): origin is string => Boolean(origin))); apiServerRef.current = apiServer; diff --git a/src/main/ipc-core-common.ts b/src/main/ipc-core-common.ts index 3bf6a0d8..75db01f8 100644 --- a/src/main/ipc-core-common.ts +++ b/src/main/ipc-core-common.ts @@ -1,5 +1,6 @@ import type { IpcMainInvokeEvent } from "electron"; import type { DesktopResult } from "@shared/contracts"; +import type { SectionId } from "@shared/types"; import type { AppDatabase } from "./database"; export type CompanyScopedTable = "workspaces" | "agents" | "goals" | "projects" | "tasks" | "approvals" | "runs" | "secrets"; @@ -30,6 +31,7 @@ export interface RegisterCoreHandlersDependencies { resetAfterRestore: () => void; getApiPort: () => number; listConnectorModels: (connectorId: string) => Promise>; + notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void; } export function ok(data: T): DesktopResult { diff --git a/src/main/ipc-core-operations-handlers.ts b/src/main/ipc-core-operations-handlers.ts index 88b06123..5f29d810 100644 --- a/src/main/ipc-core-operations-handlers.ts +++ b/src/main/ipc-core-operations-handlers.ts @@ -25,6 +25,7 @@ export function registerCoreOperationHandlers({ dispatchAutomation, queueTaskRun, getWorkerProcess, + notify, }: Pick< RegisterCoreHandlersDependencies, | "registerHandle" @@ -37,6 +38,7 @@ export function registerCoreOperationHandlers({ | "dispatchAutomation" | "queueTaskRun" | "getWorkerProcess" + | "notify" >) { registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => { const parsed = approvalInputSchema.parse(payload); @@ -81,6 +83,13 @@ export function registerCoreOperationHandlers({ agentId: parsed.requestedByAgentId ?? undefined, taskId: parsed.relatedTaskId ?? undefined, }); + notify({ + title: "Approval Required", + body: parsed.payloadSummary ?? `${parsed.type} approval pending`, + urgency: "critical", + navigation: { section: "approvals", entityId: id }, + batchKey: "pending_approval", + }); publishDomainChanged(); return ok(id); }); diff --git a/src/main/main-runtime-support.ts b/src/main/main-runtime-support.ts index d3b51286..d4354d04 100644 --- a/src/main/main-runtime-support.ts +++ b/src/main/main-runtime-support.ts @@ -145,6 +145,7 @@ export function createMainRuntimeSupport(state: MainRuntimeState) { requireBrowserActionService().resetBrowserManager(); }, getApiPort: () => state.apiServer?.getPort() ?? 0, + notify: (options) => requireIpcSupport().notify(options), listConnectorModels: async (connectorId) => { const definition = getConnectorDefinition(connectorId as Parameters[0]); return definition.listModels ? definition.listModels() : [];