Files
AgentCompany/src/main/app/ipc-handlers.ts
T
ZacharyZhang-NYandClaude Opus 5.5 245ff66336
Build Release Installers / Release for the tag (push) Successful in 16s
Build Release Installers / Windows installer (NSIS x64) (push) Successful in 1m48s
Build Release Installers / Linux packages (AppImage + deb x64) (push) Successful in 2m21s
CI / Typecheck, lint, tests, e2e (push) Successful in 3m52s
Build Release Installers / macOS installers (dmg + zip, x64 + arm64) (push) Successful in 7m15s
1.5.1: refined office, chat in the app language, current OpenAI models, own connector keys
- Home: the goal title box is two line boxes tall (2.8em), so a third line no longer peeks out.
- Chat messages, reply fallbacks and assignment DMs follow the app display language; tasks and documents keep the working language.
- OpenAI and GPT-account model lists match OpenAI's current line-up; default gpt-6.1-sol.
- Onboarding connectors step takes the operator's Composio and logo.dev keys, then lists the toolkits the Composio project enables; without a key Connectors leaves the sidebar.
- Google Antigravity removed end to end (migration 111), with the remote-revocation path only it used.
- Office: 2x pixel people with outlines, rim light, walk, typing and blink; dithered real-clock lighting; detailed furniture and floors.
- Per-agent model picker lists the provider's whole model list, live when connected.
- Chat wake context breaks same-millisecond ties by insertion order; the mid-run inbox dedupes by message id.
- The stored "clock" theme reads back instead of falling to "system".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:12:19 -04:00

480 lines
23 KiB
TypeScript

import { bumpDataVersion, currentDataVersion } from "../services/data-version";
import { createIpcGate } from "./ipc-gate";
import { app, dialog, shell, type BrowserWindow, clipboard } from "electron";
import type { DatabaseConnection } from "../database/connection";
import { IPC_CHANNELS } from "../../shared/ipc";
import { fail, ok, type DesktopResult } from "../../shared/result";
import type { AppSnapshot } from "../../shared/domain";
import { SnapshotService } from "../services/snapshot-service";
import { OrganizationService } from "../services/organization-service";
import { ApprovalService } from "../services/approval-service";
import { ChatService } from "../services/chat-service";
import { ProviderService } from "../services/provider-service";
import { ProviderOAuthService } from "../services/provider-oauth-service";
import { SkillService } from "../services/skill-service";
import { SocialService } from "../services/social-service";
import { FileService } from "../services/file-service";
import { MigrationService } from "../services/migration-service";
import { OnboardingService } from "../services/onboarding-service";
import { ComplianceService } from "../services/compliance-service";
import { SearchService } from "../services/search-service";
import { IdentityAccessService } from "../services/identity-access-service";
import { OperationsService } from "../services/operations-service";
import { TaskExecutionService } from "../services/task-execution-service";
import { DocumentService } from "../services/document-service";
import { KnowledgeService } from "../services/knowledge-service";
import { ComposioConnectionService } from "../services/composio/composio-connection-service";
import { ConnectorGrantsService } from "../services/composio/connector-grants";
import { approvalDrifts } from "../services/approval-drift";
import { spaceHistory, threadHistory } from "../services/chat-history";
import { requestsForRun } from "../services/provider-request-store";
import { setGoalAuditor } from "../services/goal-audit-inputs";
import { IntegrationKeyStore } from "../services/integration-key-store";
import { importStagedSkills, stageBuiltinSkillsLibrary } from "../services/builtin-skills";
import { coalesce } from "./coalesce";
import { normalizeExternalBrowserUrl } from "./external-url";
import { resumeGoal } from "../services/goal-review-service";
import { stopAllRuns, stopRun } from "../services/run-control";
import { cancelQueuedRun } from "../services/run-cancel";
import { runHistory } from "../services/run-history";
import { deleteWorkspace, setPrimaryWorkspace, updateWorkspace } from "../services/workspace-admin";
import { setProviderModel } from "../services/provider-default-model";
import { completeSetup, saveOnboardingDraft } from "../services/onboarding-progress";
import { setUnsavedInput } from "./unsaved-guard";
import { saveViewState } from "../services/view-state";
import { providerDisconnectImpact } from "../services/provider-disconnect-impact";
export interface IpcDeps {
connection: DatabaseConnection;
mainWindow: BrowserWindow;
/** The app's own page (dev server URL or built index.html); calls from any other page are refused. */
appPage: string;
artifactRoot: string;
/** Bundled built-in skills source (repo folder in dev, resources when packaged). */
builtinSkillsSource: string;
/** Normalized on-disk skill library under the profile directory. */
builtinSkillsLibrary: string;
}
export function registerIpcHandlers(deps: IpcDeps): void {
// Every call passes one gate: the caller must be the app's own page, and the input must match its contract.
const handle = createIpcGate(deps.mainWindow, deps.appPage);
const snapshot = new SnapshotService(deps.connection.db);
const organization = new OrganizationService(deps.connection.db);
const approvals = new ApprovalService(deps.connection.db);
const chat = new ChatService(deps.connection.db);
const providers = new ProviderService(deps.connection.db);
const integrationKeys = new IntegrationKeyStore(deps.connection.db);
const providerOAuth = new ProviderOAuthService();
const skills = new SkillService(deps.connection.db, deps.artifactRoot);
const announceChange = coalesce(() => {
try {
if (!deps.mainWindow.isDestroyed() && !deps.mainWindow.webContents.isDestroyed()) {
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed", version: currentDataVersion() });
}
} catch { /* renderer may have been disposed during agent run */ }
}, 200);
// The version moves at once, so the snapshot an action returns already carries the change its own event announces.
const notifyDataChanged = () => {
bumpDataVersion();
announceChange();
};
const taskExecution = new TaskExecutionService(deps.connection.db, providers, skills, notifyDataChanged);
const social = new SocialService(deps.connection.db);
const files = new FileService(deps.connection.db, deps.artifactRoot);
const migrations = new MigrationService(deps.connection.db);
const compliance = new ComplianceService(deps.connection.db);
const search = new SearchService(deps.connection.db);
const identityAccess = new IdentityAccessService(deps.connection.db);
const operations = new OperationsService(deps.connection.db);
const documents = new DocumentService(deps.connection.db);
const knowledge = new KnowledgeService(deps.connection.db);
const onboarding = new OnboardingService({
db: deps.connection.db,
saveProviderCredential: (input) => providers.saveCredential(input),
});
const mutate = <T>(operation: () => T): DesktopResult<AppSnapshot> => {
try {
deps.connection.transaction(operation);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("operation_failed", error instanceof Error ? error.message : String(error));
}
};
const query = <T>(code: string, read: () => T): DesktopResult<T> => {
try {
return ok(read());
} catch (error) {
return fail(code, error instanceof Error ? error.message : String(error));
}
};
handle("loadSnapshot", () => {
try {
return ok(snapshot.load());
} catch (error) {
return fail("snapshot_failed", error instanceof Error ? error.message : String(error));
}
});
handle("bootstrapOnboarding", (input) => {
try {
let summary: { orgId: string } | undefined;
// Staging happens before the transaction (file mirroring), the import
// inside it: the CEO's kickoff run can be claimed within seconds of
// commit and must already find the forced humanizer skill.
const staged = stageBuiltinSkillsLibrary(deps.builtinSkillsSource, deps.builtinSkillsLibrary);
deps.connection.transaction(() => {
summary = onboarding.bootstrap(input);
const report = importStagedSkills(skills, summary.orgId, staged);
if (report.failures.length > 0) {
console.warn(`[skills] onboarding import failures (${report.failures.length}): ${report.failures.slice(0, 3).join("; ")}`);
}
});
notifyDataChanged();
return ok(summary);
} catch (error) {
return fail("onboarding_failed", error instanceof Error ? error.message : String(error));
}
});
handle("createOrganization", (input) => mutate(() => organization.createOrganization(input)));
handle("setActiveOrganization", (input) => mutate(() => organization.setActiveOrganization(input.orgId)));
handle("setUiPreferences", (input) => mutate(() => organization.updateUiPreferences(input)));
handle("saveOnboardingDraft", (input) => {
try {
saveOnboardingDraft(deps.connection.db, input.draft);
return ok(null);
} catch (error) {
return fail("onboarding_draft_failed", error instanceof Error ? error.message : String(error));
}
});
handle("completeSetup", (input) => mutate(() => completeSetup(deps.connection.db, input.orgId)));
handle("copyText", (input) => {
clipboard.writeText(input.text);
return ok(null);
});
handle("saveViewState", (input) => {
try {
saveViewState(deps.connection.db, input.key, input.value);
return ok(null);
} catch (error) {
return fail("view_state_failed", error instanceof Error ? error.message : String(error));
}
});
handle("reportUnsaved", (input) => {
setUnsavedInput(input.unsaved);
return ok(null);
});
handle("runningSessionCount", () => ok((deps.connection.db.prepare("select count(*) as n from task_runs where state = 'running'").get() as { n: number }).n));
// Chromium's locale is fixed at launch; the reply goes out before the process exits.
handle("relaunchApp", () => {
console.info("[app] relaunch requested by the user");
setTimeout(() => {
app.relaunch();
app.quit();
}, 150);
return ok(null);
});
handle("deleteOrganization", (input) => mutate(() => organization.deleteOrganization(input.orgId)));
handle("createAgent", (input) => mutate(() => organization.createAgent(input)));
handle("updateAgent", (input) => mutate(() => organization.updateAgent(input)));
handle("updateEmployeeProfile", (input) => mutate(() => organization.updateEmployeeProfile(input)));
handle("setAutonomousPaused", (input) => mutate(() => {
organization.setAutonomousPaused(Boolean(input.paused));
// Pausing also stops the sessions already running, not just future scheduling.
if (input.paused) stopAllRuns();
}));
handle("stopRun", (input) => mutate(() => {
if (!stopRun(input.runId)) throw new Error("That run is not running anymore.");
}));
handle("setIntegrationKey", (input) => mutate(() => integrationKeys.write(input.name, input.value)));
handle("clearIntegrationKey", (input) => mutate(() => integrationKeys.clear(input.name)));
handle("createTask", (input) => mutate(() => organization.createTask(input)));
handle("updateTask", (input) => mutate(() => organization.updateTask(input)));
handle("setTaskBlocked", (input) => mutate(() => organization.setTaskBlocked(input)));
handle("taskDeleteImpact", (input) => ok(organization.taskDeleteImpact(input.orgId, input.taskId)));
handle("cancelQueuedRun", (input) => mutate(() => cancelQueuedRun(deps.connection.db, input.orgId, input.runId)));
handle("runHistory", (input) => ok(runHistory(deps.connection.db, input.orgId, input.beforeRunId, "limit" in input ? { limit: input.limit } : { from: input.from })));
handle("deleteTask", (input) => mutate(() => organization.deleteTask(input)));
handle("updateTaskStatus", (input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
handle("runTask", async (input) => {
try {
await taskExecution.runTask(input);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
notifyDataChanged();
return fail("task_run_failed", error instanceof Error ? error.message : String(error));
}
});
handle("createGoal", (input) => mutate(() => organization.createGoal(input)));
handle("setGoalDueDate", (input) => mutate(() => organization.setGoalDueDate(input)));
handle("updateOrganization", (input) => mutate(() => organization.updateOrganization(input)));
handle("updateWorkspace", (input) => mutate(() => updateWorkspace(deps.connection.db, input)));
handle("setPrimaryWorkspace", (input) => mutate(() => setPrimaryWorkspace(deps.connection.db, input.orgId, input.workspaceId)));
handle("deleteWorkspace", (input) => mutate(() => deleteWorkspace(deps.connection.db, input.orgId, input.workspaceId)));
handle("createProject", (input) => mutate(() => organization.createProject(input)));
handle("createWorkspace", (input) => mutate(() => organization.createWorkspace(input)));
handle("createDocument", (input) => mutate(() => documents.create(input)));
handle("updateDocument", (input) => mutate(() => documents.update(input)));
handle("deleteDocument", (input) => mutate(() => documents.delete(input)));
handle("createKnowledgeEntry", (input) => mutate(() => knowledge.create(input)));
handle("deleteKnowledgeEntry", (input) => mutate(() => knowledge.delete(input)));
handle("resumeGoal", (input) => mutate(() => resumeGoal(deps.connection.db, organization, input.orgId, input.goalId)));
handle("updateKnowledgeEntry", (input) => mutate(() => knowledge.update(input)));
handle("listDocumentVersions", (input) => query("document_versions_failed", () =>
documents.versions(input.orgId, input.documentId).map((v) => ({
version: v.version, title: v.title, content: v.content, status: v.status,
changedByType: v.changed_by_type, changedById: v.changed_by_id, reason: v.reason, createdAt: v.created_at,
}))));
handle("listKnowledgeRevisions", (input) => query("knowledge_revisions_failed", () =>
knowledge.revisions(input.orgId, input.entryId).map((r) => ({
revision: r.revision, topic: r.topic, category: r.category, content: r.content, importance: r.importance,
changedByType: r.changed_by_type, changedById: r.changed_by_id, reason: r.reason, createdAt: r.created_at,
}))));
handle("createApproval", (input) => mutate(() => approvals.create(input)));
handle("decideApproval", (input) => mutate(() => approvals.decide(input)));
handle("syncEmployeeIdentity", (input) => mutate(() => identityAccess.syncEmployeeIdentity(input)));
handle("grantRole", (input) => mutate(() => identityAccess.grantRole(input)));
handle("createChatSpace", (input) => mutate(() => chat.createSpace(input)));
handle("sendMessage", (input) => mutate(() => chat.sendMessage(input)));
handle("editMessage", (input) => mutate(() => chat.editMessage(input)));
handle("deleteMessage", (input) => mutate(() => chat.deleteMessage(input)));
handle("markChatSpaceRead", (input) => mutate(() => chat.markSpaceRead(input)));
handle("saveProviderCredential", (input) => mutate(() => providers.saveCredential(input)));
handle("providerEndpoint", (input) => {
try {
return ok(providers.endpoint(input.orgId, input.providerId));
} catch (error) {
return fail("provider_endpoint_failed", error instanceof Error ? error.message : String(error));
}
});
handle("providerDisconnectImpact", (input) => {
try {
return ok(providerDisconnectImpact(deps.connection.db, input.orgId, input.providerId));
} catch (error) {
return fail("provider_impact_failed", error instanceof Error ? error.message : String(error));
}
});
handle("setProviderModel", (input) => mutate(() => setProviderModel(deps.connection.db, input)));
handle("revokeProviderCredential", (input) => mutate(() => providers.revokeCredential(input)));
handle("updateProviderPolicy", (input) => mutate(() => providers.updatePolicy(input)));
handle("setProviderPrices", (input) => mutate(() => providers.setPrices(input)));
handle("setGoalAuditor", (input) => mutate(() => setGoalAuditor(deps.connection.db, input)));
handle("checkProviderHealth", async (input) => {
try {
await providers.checkHealth(input.orgId, input.providerId);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("provider_health_failed", error instanceof Error ? error.message : String(error));
}
});
handle("listProviderModels", async (input) => {
try {
return ok(await providers.listModels(input));
} catch (error) {
return fail("provider_models_failed", error instanceof Error ? error.message : String(error));
}
});
handle("checkProviderModel", async (input) => {
try {
return ok(await providers.checkModel(input));
} catch (error) {
return fail("provider_model_check_failed", error instanceof Error ? error.message : String(error));
}
});
handle("startProviderOAuth", async (input) => {
const emit = (event: Record<string, unknown>) =>
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "provider-oauth", providerKey: input.providerKey, ...event });
try {
const secret = await providerOAuth.run(
{ providerKey: input.providerKey, enterpriseDomain: input.enterpriseDomain ?? null },
{
onAuth: ({ url, userCode }) => {
const browserUrl = normalizeExternalBrowserUrl(url);
void shell.openExternal(browserUrl);
emit({ phase: "authorize", url: browserUrl, userCode });
},
onProgress: (message) => emit({ phase: "progress", message }),
},
);
if (input.orgId && input.providerId && input.employeeId) {
providers.saveCredential({
orgId: input.orgId,
providerId: input.providerId,
secret,
model: input.model ?? null,
});
emit({ phase: "active" });
notifyDataChanged();
return ok({ secret: null });
}
emit({ phase: "active" });
return ok({ secret });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
emit({ phase: message === "Login cancelled" ? "cancelled" : "error", message });
return fail("provider_oauth_failed", message);
}
});
handle("cancelProviderOAuth", (input) => {
providerOAuth.cancel(input.providerKey);
return ok(null);
});
handle("openExternalUrl", async (url) => {
try {
await shell.openExternal(normalizeExternalBrowserUrl(url));
return ok(null);
} catch (error) {
return fail("external_url_failed", error instanceof Error ? error.message : String(error));
}
});
handle("createProviderResponse", async (input) => {
try {
await providers.createResponse(input);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("provider_response_failed", error instanceof Error ? error.message : String(error));
}
});
handle("streamProviderResponse", async (input) => {
try {
await providers.streamResponse(input, (streamEvent) => {
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "provider-response-stream", event: streamEvent });
});
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("provider_stream_failed", error instanceof Error ? error.message : String(error));
}
});
handle("cancelProviderResponse", async (input) => {
try {
await providers.cancelResponse(input);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("provider_cancel_failed", error instanceof Error ? error.message : String(error));
}
});
handle("createSocialAccount", (input) => mutate(() => social.createAccount(input)));
handle("createScheduledPost", (input) => mutate(() => social.createScheduledPost(input)));
handle("setScheduledPostState", (input) => mutate(() => social.setScheduledPostState(input)));
handle("setFilePinned", (input) => mutate(() => files.setPinned(input.orgId, input.fileId, input.pinned)));
handle("launchSocialLogin", (input) => {
try {
social.launchLoginWindow(input);
return ok(null);
} catch (error) {
return fail("social_login_failed", error instanceof Error ? error.message : String(error));
}
});
handle("importFile", async (input) => {
try {
const imported = await files.importFile(input);
if (imported.fileIds.length > 0) notifyDataChanged();
return ok(imported);
} catch (error) {
return fail("file_import_failed", error instanceof Error ? error.message : String(error));
}
});
handle("openFile", async (input) => {
try {
await files.openFile(input);
return ok(null);
} catch (error) {
return fail("file_open_failed", error instanceof Error ? error.message : String(error));
}
});
handle("pickDirectory", async () => {
try {
const result = await dialog.showOpenDialog(deps.mainWindow, { properties: ["openDirectory", "createDirectory"] });
if (result.canceled || result.filePaths.length === 0) return ok(null);
return ok(result.filePaths[0]);
} catch (error) {
return fail("pick_directory_failed", error instanceof Error ? error.message : String(error));
}
});
handle("createMigrationReport", (input) => mutate(() => migrations.createReport(input)));
handle("setFeatureFlag", (input) => mutate(() => operations.setFeatureFlag(input)));
handle("createComplianceExport", (input) => mutate(() => compliance.createExport(input)));
handle("createLegalHold", (input) => mutate(() => compliance.createLegalHold(input)));
handle("setLegalHoldState", (input) => mutate(() => compliance.setLegalHoldState(input)));
handle("createRetentionFreeze", (input) => mutate(() => compliance.createRetentionFreeze(input)));
handle("setRetentionFreezeState", (input) => mutate(() => compliance.setRetentionFreezeState(input)));
handle("search", (input) => {
try {
return ok(search.search(input));
} catch (error) {
return fail("search_failed", error instanceof Error ? error.message : String(error));
}
});
const composioConnections = new ComposioConnectionService(deps.connection.db, integrationKeys);
handle("listConnectorCatalog", async () => {
try {
return ok(await composioConnections.listCatalog());
} catch (error) {
return fail("composio_catalog_failed", error instanceof Error ? error.message : String(error));
}
});
handle("connectComposioToolkit", async (input) => {
try {
return ok(await composioConnections.connectToolkit(input.orgId, input.toolkitSlug, input.label));
} catch (error) {
return fail("composio_connect_failed", error instanceof Error ? error.message : String(error));
} finally {
// A connection change that failed partway may still have written; the page reloads either way.
notifyDataChanged();
}
});
handle("disconnectComposioToolkit", async (input) => {
try {
await composioConnections.disconnectToolkit(input.orgId, input.toolkitSlug, input.label);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
// A disconnect that failed partway may still have written; the page reloads either way.
notifyDataChanged();
return fail("composio_disconnect_failed", error instanceof Error ? error.message : String(error));
}
});
handle("grantConnector", (input) => mutate(() => new ConnectorGrantsService(deps.connection.db).grantFromBoard(input)));
handle("runRequests", (input) => ok(requestsForRun(deps.connection.db, input.orgId, input.runId)));
handle("chatThreadHistory", (input) => ok(threadHistory(deps.connection.db, input.orgId, input.threadId)));
handle("chatSpaceHistory", (input) => ok(spaceHistory(deps.connection.db, input.orgId, input.spaceId, input.beforeMessageId, "limit" in input ? { limit: input.limit } : { fromMessageId: input.fromMessageId })));
handle("approvalDrifts", (input) => {
try {
return ok(approvalDrifts(deps.connection.db, input.orgId, input.approvalIds));
} catch (error) {
return fail("approval_drift_failed", error instanceof Error ? error.message : String(error));
}
});
handle("revokeConnector", (input) => mutate(() => new ConnectorGrantsService(deps.connection.db).revoke(input)));
handle("refreshComposioAccounts", async (input) => {
try {
await composioConnections.refreshAccounts(input.orgId);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
notifyDataChanged();
return fail("composio_refresh_failed", error instanceof Error ? error.message : String(error));
}
});
handle("windowMinimize", () => deps.mainWindow.minimize());
handle("windowMaximize", () => deps.mainWindow.isMaximized() ? deps.mainWindow.unmaximize() : deps.mainWindow.maximize());
handle("windowClose", () => deps.mainWindow.close());
}