fix(04): wire pending approval notifications in IPC and API server paths

This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 17:12:40 -04:00
1 parent 9d01a7acc4
commit a2a110f0c5
5 files changed
+26 -2

No files matched your search

+11 -1
View File
@@ -9,7 +9,7 @@ import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-p
import type { AppDatabase } from "./database"; import type { AppDatabase } from "./database";
import { inferTaskType } from "./connector-matching"; import { inferTaskType } from "./connector-matching";
import { dispatchAgentMessage } from "./message-dispatch"; 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 * 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; onDocumentCreated: (companyId: string, authorAgentId: string | null, projectId: string | null) => void;
onAutoAssignTask: (taskId: string, companyId: string) => string | null; onAutoAssignTask: (taskId: string, companyId: string) => string | null;
emitEvent: (event: DesktopEvent) => void; emitEvent: (event: DesktopEvent) => void;
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
} }
export class AgentApiServer { export class AgentApiServer {
@@ -114,6 +115,7 @@ export class AgentApiServer {
onDocumentCreated: callbacks?.onDocumentCreated ?? (() => {}), onDocumentCreated: callbacks?.onDocumentCreated ?? (() => {}),
onAutoAssignTask: callbacks?.onAutoAssignTask ?? (() => null), onAutoAssignTask: callbacks?.onAutoAssignTask ?? (() => null),
emitEvent: callbacks?.emitEvent ?? (() => {}), emitEvent: callbacks?.emitEvent ?? (() => {}),
notify: callbacks?.notify ?? (() => {}),
}; };
this.registerRoutes(); this.registerRoutes();
} }
@@ -1009,6 +1011,14 @@ export class AgentApiServer {
detail: "Auto-approved by company policy", detail: "Auto-approved by company policy",
}); });
log.info(`[auto-approve] Auto-approved approval ${id}: ${input.payloadSummary}`); 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(); this.onDomainChanged();
+3 -1
View File
@@ -8,7 +8,7 @@ import { createServiceGraph } from "./service-graph";
import { createIpcSupport } from "./ipc-support"; import { createIpcSupport } from "./ipc-support";
import { createNotificationService } from "./notification-service"; import { createNotificationService } from "./notification-service";
import type { NotificationOptions } from "./notification-service"; import type { NotificationOptions } from "./notification-service";
import type { DesktopEvent } from "@shared/types"; import type { DesktopEvent, SectionId } from "@shared/types";
interface BootstrapApplicationDependencies { interface BootstrapApplicationDependencies {
hasSingleInstanceLock: boolean; hasSingleInstanceLock: boolean;
@@ -33,6 +33,7 @@ interface BootstrapApplicationDependencies {
onDocumentCreated: (companyId: string, authorAgentId: string | null, projectId: string | null) => void; onDocumentCreated: (companyId: string, authorAgentId: string | null, projectId: string | null) => void;
onAutoAssignTask: (taskId: string, companyId: string) => string | null; onAutoAssignTask: (taskId: string, companyId: string) => string | null;
emitEvent: (event: DesktopEvent) => void; emitEvent: (event: DesktopEvent) => void;
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
}, allowedOrigins: string[]) => AgentApiServer; }, allowedOrigins: string[]) => AgentApiServer;
createRunToken: (agentId: string, companyId: string, runId: string) => string; createRunToken: (agentId: string, companyId: string, runId: string) => string;
seedDefaultAutomationRules: (companyId: string) => void; seedDefaultAutomationRules: (companyId: string) => void;
@@ -168,6 +169,7 @@ export async function bootstrapApplication({
}, },
onAutoAssignTask: (taskId, companyId) => db.autoAssignTask(taskId, companyId), onAutoAssignTask: (taskId, companyId) => db.autoAssignTask(taskId, companyId),
emitEvent, emitEvent,
notify,
}, [isDev && devServerUrl ? new URL(devServerUrl).origin : null, "app://-"].filter((origin): origin is string => Boolean(origin))); }, [isDev && devServerUrl ? new URL(devServerUrl).origin : null, "app://-"].filter((origin): origin is string => Boolean(origin)));
apiServerRef.current = apiServer; apiServerRef.current = apiServer;
+2
View File
@@ -1,5 +1,6 @@
import type { IpcMainInvokeEvent } from "electron"; import type { IpcMainInvokeEvent } from "electron";
import type { DesktopResult } from "@shared/contracts"; import type { DesktopResult } from "@shared/contracts";
import type { SectionId } from "@shared/types";
import type { AppDatabase } from "./database"; import type { AppDatabase } from "./database";
export type CompanyScopedTable = "workspaces" | "agents" | "goals" | "projects" | "tasks" | "approvals" | "runs" | "secrets"; export type CompanyScopedTable = "workspaces" | "agents" | "goals" | "projects" | "tasks" | "approvals" | "runs" | "secrets";
@@ -30,6 +31,7 @@ export interface RegisterCoreHandlersDependencies {
resetAfterRestore: () => void; resetAfterRestore: () => void;
getApiPort: () => number; getApiPort: () => number;
listConnectorModels: (connectorId: string) => Promise<Array<{ id: string; label: string }>>; listConnectorModels: (connectorId: string) => Promise<Array<{ id: string; label: string }>>;
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
} }
export function ok<T>(data: T): DesktopResult<T> { export function ok<T>(data: T): DesktopResult<T> {
+9
View File
@@ -25,6 +25,7 @@ export function registerCoreOperationHandlers({
dispatchAutomation, dispatchAutomation,
queueTaskRun, queueTaskRun,
getWorkerProcess, getWorkerProcess,
notify,
}: Pick< }: Pick<
RegisterCoreHandlersDependencies, RegisterCoreHandlersDependencies,
| "registerHandle" | "registerHandle"
@@ -37,6 +38,7 @@ export function registerCoreOperationHandlers({
| "dispatchAutomation" | "dispatchAutomation"
| "queueTaskRun" | "queueTaskRun"
| "getWorkerProcess" | "getWorkerProcess"
| "notify"
>) { >) {
registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => { registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => {
const parsed = approvalInputSchema.parse(payload); const parsed = approvalInputSchema.parse(payload);
@@ -81,6 +83,13 @@ export function registerCoreOperationHandlers({
agentId: parsed.requestedByAgentId ?? undefined, agentId: parsed.requestedByAgentId ?? undefined,
taskId: parsed.relatedTaskId ?? 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(); publishDomainChanged();
return ok(id); return ok(id);
}); });
+1
View File
@@ -145,6 +145,7 @@ export function createMainRuntimeSupport(state: MainRuntimeState) {
requireBrowserActionService().resetBrowserManager(); requireBrowserActionService().resetBrowserManager();
}, },
getApiPort: () => state.apiServer?.getPort() ?? 0, getApiPort: () => state.apiServer?.getPort() ?? 0,
notify: (options) => requireIpcSupport().notify(options),
listConnectorModels: async (connectorId) => { listConnectorModels: async (connectorId) => {
const definition = getConnectorDefinition(connectorId as Parameters<typeof getConnectorDefinition>[0]); const definition = getConnectorDefinition(connectorId as Parameters<typeof getConnectorDefinition>[0]);
return definition.listModels ? definition.listModels() : []; return definition.listModels ? definition.listModels() : [];