import { z } from 'zod'; import { RelPath, Encoding, Stat, DirEntry, ReaddirOptions, ReadFileResult, WorkspaceChangeEvent, WriteFileOptions, WriteFileResult, RemoveOptions } from './workspace.js'; import { ListToolsResponse } from './mcp.js'; import { AskHumanResponsePayload, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload } from './runs.js'; import { LlmModelConfig, LlmProvider } from './models.js'; import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js'; import { AgentScheduleState } from './agent-schedule-state.js'; import { ServiceEvent } from './service-events.js'; import { LiveNoteAgentEvent, LiveNoteSchema } from './live-note.js'; import { BackgroundTaskAgentEvent, BackgroundTaskSchema, BackgroundTaskSummarySchema, TriggersSchema, } from './background-task.js'; import { UserMessage, UserMessageContent } from './message.js'; import { RequestedAgent, type TurnEvent } from './turns.js'; import type { SessionBusEvent, SessionIndexEntry, SessionState } from './sessions.js'; import { RowboatApiConfig } from './rowboat-account.js'; import { ZListToolkitsResponse } from './composio.js'; import { BrowserStateSchema, HttpAuthRequestSchema } from './browser-control.js'; import { BillingInfoSchema } from './billing.js'; import { EmailBlockSchema, GmailThreadSchema } from './blocks.js'; import { PermissionDecision, ApprovalPolicy, CodingAgent } from './code-mode.js'; import { NotificationSettingsSchema } from './notification-settings.js'; import { CodeProject, CodeSession, CodeSessionMode, CodeSessionStatus, GitRepoInfo, GitStatusFile, CodeAgentModelOptions } from './code-sessions.js'; // ============================================================================ // Runtime Validation Schemas (Single Source of Truth) // ============================================================================ const KnowledgeSourceScopeSchema = z.object({ type: z.string(), id: z.string(), name: z.string().optional(), workspaceUrl: z.string().optional(), }); // Mirrors AgentSlackErrorKind in @x/core/slack/agent-slack-exec. Kept as a // standalone enum so the renderer can branch on failure cause without // importing core. const SlackErrorKindSchema = z.enum([ 'not_installed', 'timeout', 'parse_error', 'not_authed', 'rate_limited', 'network', 'bad_channel', 'unknown', ]); const KnowledgeSourceConfigSchema = z.object({ id: z.string(), provider: z.enum(['gmail', 'meeting', 'voice_memo', 'slack', 'github', 'linear']), enabled: z.boolean(), artifactDir: z.string(), syncMode: z.enum(['file', 'poll', 'event', 'manual']).default('file'), intervalMs: z.number().int().positive().optional(), scopes: z.array(KnowledgeSourceScopeSchema).default([]), instructions: z.string().optional(), filters: z.record(z.string(), z.unknown()).optional(), }); const ipcSchemas = { 'app:getVersions': { req: z.null(), res: z.object({ chrome: z.string(), node: z.string(), electron: z.string(), }), }, 'analytics:bootstrap': { req: z.null(), res: z.object({ installationId: z.string(), apiUrl: z.string(), appVersion: z.string(), }), }, 'workspace:getRoot': { req: z.null(), res: z.object({ root: z.string(), }), }, 'workspace:exists': { req: z.object({ path: RelPath, }), res: z.object({ exists: z.boolean(), }), }, 'workspace:stat': { req: z.object({ path: RelPath, }), res: Stat, }, 'workspace:readdir': { req: z.object({ path: z.string(), // Empty string allowed for root directory opts: ReaddirOptions.optional(), }), res: z.array(DirEntry), }, 'workspace:readFile': { req: z.object({ path: RelPath, encoding: Encoding.optional(), }), res: ReadFileResult, }, 'workspace:writeFile': { req: z.object({ path: RelPath, data: z.string(), opts: WriteFileOptions.optional(), }), res: WriteFileResult, }, 'workspace:mkdir': { req: z.object({ path: RelPath, recursive: z.boolean().optional(), }), res: z.object({ ok: z.literal(true), }), }, 'workspace:rename': { req: z.object({ from: RelPath, to: RelPath, overwrite: z.boolean().optional(), }), res: z.object({ ok: z.literal(true), }), }, 'workspace:copy': { req: z.object({ from: RelPath, to: RelPath, overwrite: z.boolean().optional(), }), res: z.object({ ok: z.literal(true), }), }, 'workspace:remove': { req: z.object({ path: RelPath, opts: RemoveOptions.optional(), }), res: z.object({ ok: z.literal(true), }), }, 'workspace:didChange': { req: WorkspaceChangeEvent, res: z.null(), }, 'gmail:getImportant': { req: z.object({ cursor: z.string().optional(), limit: z.number().int().min(1).max(100).optional(), }), res: z.object({ threads: z.array(GmailThreadSchema), nextCursor: z.string().nullable(), }), }, 'gmail:getEverythingElse': { req: z.object({ cursor: z.string().optional(), limit: z.number().int().min(1).max(100).optional(), }), res: z.object({ threads: z.array(GmailThreadSchema), nextCursor: z.string().nullable(), }), }, 'gmail:triggerSync': { req: z.object({}), res: z.object({}), }, 'gmail:sendReply': { req: z.object({ threadId: z.string().min(1).optional(), to: z.string().min(1), cc: z.string().optional(), bcc: z.string().optional(), subject: z.string(), bodyHtml: z.string(), bodyText: z.string(), inReplyTo: z.string().optional(), references: z.string().optional(), attachments: z .array( z.object({ filename: z.string(), mimeType: z.string(), contentBase64: z.string(), }), ) .optional(), }), res: z.object({ messageId: z.string().optional(), error: z.string().optional(), }), }, 'gmail:saveDraft': { req: z.object({ // Existing Gmail draft to update; omitted on first save (creates a new one). draftId: z.string().min(1).optional(), threadId: z.string().min(1).optional(), // Recipients may be blank for a draft (unlike a send). to: z.string().optional(), cc: z.string().optional(), bcc: z.string().optional(), subject: z.string(), bodyHtml: z.string(), bodyText: z.string(), inReplyTo: z.string().optional(), references: z.string().optional(), attachments: z .array( z.object({ filename: z.string(), mimeType: z.string(), contentBase64: z.string(), }), ) .optional(), }), res: z.object({ draftId: z.string().optional(), error: z.string().optional(), }), }, 'gmail:deleteDraft': { req: z.object({ draftId: z.string().min(1) }), res: z.object({ ok: z.boolean(), error: z.string().optional() }), }, 'gmail:getDrafts': { req: z.object({}), res: z.object({ threads: z.array(GmailThreadSchema), error: z.string().optional(), }), }, 'gmail:search': { req: z.object({ query: z.string(), limit: z.number().int().positive().optional(), }), res: z.object({ threads: z.array(GmailThreadSchema), error: z.string().optional(), }), }, 'gmail:getConnectionStatus': { req: z.object({}), res: z.object({ connected: z.boolean(), hasRequiredScope: z.boolean(), missingScopes: z.array(z.string()), email: z.string().nullable(), }), }, 'gmail:getAccountEmail': { req: z.object({}), res: z.object({ email: z.string().nullable(), }), }, 'gmail:getAccountName': { req: z.object({}), res: z.object({ name: z.string().nullable(), }), }, 'gmail:archiveThread': { req: z.object({ threadId: z.string().min(1) }), res: z.object({ ok: z.boolean(), error: z.string().optional() }), }, 'gmail:trashThread': { req: z.object({ threadId: z.string().min(1) }), res: z.object({ ok: z.boolean(), error: z.string().optional() }), }, 'gmail:markThreadRead': { req: z.object({ threadId: z.string().min(1), read: z.boolean().optional() }), res: z.object({ ok: z.boolean(), error: z.string().optional() }), }, 'gmail:downloadAttachment': { req: z.object({ messageId: z.string().min(1), savedPath: z.string().min(1), attachmentId: z.string().optional(), }), res: z.object({ ok: z.boolean(), error: z.string().optional() }), }, 'gmail:saveMessageHeight': { req: z.object({ threadId: z.string().min(1), messageId: z.string().min(1), height: z.number().int().positive(), }), res: z.object({}), }, 'gmail:searchContacts': { req: z.object({ query: z.string(), limit: z.number().int().positive().optional(), excludeEmails: z.array(z.string()).optional(), }), res: z.object({ contacts: z.array(z.object({ name: z.string(), email: z.string(), count: z.number(), lastSeenMs: z.number(), })), }), }, 'mcp:listTools': { req: z.object({ serverName: z.string(), cursor: z.string().optional(), }), res: ListToolsResponse, }, 'mcp:executeTool': { req: z.object({ serverName: z.string(), toolName: z.string(), input: z.record(z.string(), z.unknown()), }), res: z.object({ result: z.unknown(), }), }, 'runs:create': { req: CreateRunOptions, res: Run, }, 'runs:createMessage': { req: z.object({ runId: z.string(), message: UserMessageContent, voiceInput: z.boolean().optional(), voiceOutput: z.enum(['summary', 'full']).optional(), searchEnabled: z.boolean().optional(), codeMode: z.enum(['claude', 'codex']).optional(), // Code-section sessions pin the coding agent's working directory and // approval policy for the whole turn (see code_agent_run overrides). codeCwd: z.string().optional(), codePolicy: ApprovalPolicy.optional(), middlePaneContext: z.discriminatedUnion('kind', [ z.object({ kind: z.literal('note'), path: z.string(), content: z.string(), }), z.object({ kind: z.literal('browser'), url: z.string(), title: z.string(), }), ]).optional(), }), res: z.object({ messageId: z.string(), }), }, 'runs:authorizePermission': { req: z.object({ runId: z.string(), authorization: ToolPermissionAuthorizePayload, }), res: z.object({ success: z.literal(true), }), }, 'runs:provideHumanInput': { req: z.object({ runId: z.string(), reply: AskHumanResponsePayload, }), res: z.object({ success: z.literal(true), }), }, 'runs:stop': { req: z.object({ runId: z.string(), force: z.boolean().optional().default(false), }), res: z.object({ success: z.literal(true), }), }, 'runs:fetch': { req: z.object({ runId: z.string(), }), res: Run, }, 'runs:list': { req: z.object({ cursor: z.string().optional(), }), res: ListRunsResponse, }, 'runs:listByWorkDir': { req: z.object({ dir: z.string(), }), res: ListRunsResponse, }, 'runs:delete': { req: z.object({ runId: z.string(), }), res: z.object({ success: z.boolean() }), }, 'runs:downloadLog': { req: z.object({ runId: z.string().min(1), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'runs:events': { req: z.null(), res: z.null(), }, // ── New runtime: sessions + turns (session-design.md) ──────────────────── // Turn-mutating calls return quickly; the renderer follows progress through // the sessions:events feed and the shared reduceTurn reducer. 'sessions:create': { req: z.object({ title: z.string().optional() }), res: z.object({ sessionId: z.string() }), }, 'sessions:list': { req: z.object({}), res: z.object({ sessions: z.array(z.custom()) }), }, 'sessions:get': { req: z.object({ sessionId: z.string() }), res: z.custom(), }, 'sessions:getTurn': { // Events are strictly validated at the repository read; typed via // z.custom to avoid re-validating potentially large logs per IPC hop. req: z.object({ turnId: z.string() }), res: z.custom<{ turnId: string; events: Array> }>(), }, 'sessions:sendMessage': { req: z.object({ sessionId: z.string(), input: UserMessage, config: z.object({ agent: RequestedAgent, autoPermission: z.boolean().optional(), maxModelCalls: z.number().int().positive().optional(), }), }), res: z.object({ turnId: z.string() }), }, 'sessions:respondToPermission': { req: z.object({ turnId: z.string(), toolCallId: z.string(), decision: z.enum(['allow', 'deny']), metadata: z.json().optional(), }), res: z.object({ success: z.literal(true) }), }, 'sessions:respondToAskHuman': { req: z.object({ turnId: z.string(), toolCallId: z.string(), answer: z.string(), }), res: z.object({ success: z.literal(true) }), }, 'sessions:stopTurn': { req: z.object({ turnId: z.string(), reason: z.string().optional(), }), res: z.object({ success: z.literal(true) }), }, 'sessions:resumeTurn': { req: z.object({ sessionId: z.string() }), res: z.object({ success: z.literal(true) }), }, 'sessions:setTitle': { req: z.object({ sessionId: z.string(), title: z.string() }), res: z.object({ success: z.literal(true) }), }, 'sessions:downloadLog': { // Concatenates the session's turn logs into one JSONL for debugging. req: z.object({ sessionId: z.string() }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'sessions:delete': { req: z.object({ sessionId: z.string() }), res: z.object({ success: z.literal(true) }), }, 'sessions:events': { // Typed via z.custom so the renderer's `on` handler is typed without // runtime validation (the broadcast path bypasses preload validation, // like runs:events). req: z.custom(), res: z.null(), }, 'services:events': { req: ServiceEvent, res: z.null(), }, 'live-note-agent:events': { req: LiveNoteAgentEvent, res: z.null(), }, 'bg-task-agent:events': { req: BackgroundTaskAgentEvent, res: z.null(), }, 'models:list': { req: z.null(), res: z.object({ providers: z.array(z.object({ id: z.string(), name: z.string(), models: z.array(z.object({ id: z.string(), name: z.string().optional(), release_date: z.string().optional(), })), })), lastUpdated: z.string().optional(), }), }, 'models:test': { req: LlmModelConfig, res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'models:listForProvider': { req: z.object({ provider: LlmProvider, }), res: z.object({ success: z.boolean(), models: z.array(z.string()).optional(), error: z.string().optional(), }), }, 'llm:getDefaultModel': { req: z.null(), res: z.object({ model: z.string(), provider: z.string(), }), }, 'llm:generate': { req: z.object({ prompt: z.string().min(1), system: z.string().optional(), model: z.string().optional(), provider: z.string().optional(), }), res: z.object({ text: z.string().optional(), model: z.string().optional(), provider: z.string().optional(), error: z.string().optional(), }), }, 'models:saveConfig': { req: LlmModelConfig, res: z.object({ success: z.literal(true), }), }, 'oauth:connect': { req: z.object({ provider: z.string(), clientId: z.string().optional(), clientSecret: z.string().optional(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'oauth:disconnect': { req: z.object({ provider: z.string(), }), res: z.object({ success: z.boolean(), }), }, 'oauth:list-providers': { req: z.null(), res: z.object({ providers: z.array(z.string()), }), }, 'oauth:getState': { req: z.null(), res: z.object({ config: z.record(z.string(), z.object({ connected: z.boolean(), error: z.string().nullable().optional(), userId: z.string().optional(), clientId: z.string().nullable().optional(), })), }), }, 'account:getRowboat': { req: z.null(), res: z.object({ signedIn: z.boolean(), accessToken: z.string().nullable(), config: RowboatApiConfig.nullable(), }), }, 'oauth:didConnect': { req: z.object({ provider: z.string(), success: z.boolean(), error: z.string().optional(), userId: z.string().optional(), }), res: z.null(), }, 'app:openUrl': { req: z.object({ url: z.string(), }), res: z.null(), }, 'app:takeMeetingNotes': { req: z.object({ // Pass the raw calendar event JSON through; renderer adapts to its existing flow. event: z.unknown(), // When true, the renderer should also open the meeting URL (Zoom/Meet/etc.) // in addition to triggering the take-notes flow. openMeeting: z.boolean().optional(), }), res: z.null(), }, 'app:consumePendingDeepLink': { req: z.null(), res: z.object({ url: z.string().nullable(), }), }, 'granola:getConfig': { req: z.null(), res: z.object({ enabled: z.boolean(), }), }, 'codeMode:getConfig': { req: z.null(), res: z.object({ enabled: z.boolean(), approvalPolicy: ApprovalPolicy.optional(), }), }, 'codeMode:setConfig': { req: z.object({ enabled: z.boolean(), approvalPolicy: ApprovalPolicy.optional(), }), res: z.object({ success: z.literal(true), }), }, // Answer a mid-run permission request from a code_agent_run coding turn. 'codeRun:resolvePermission': { req: z.object({ requestId: z.string(), decision: PermissionDecision, }), res: z.object({ success: z.literal(true), }), }, 'codeMode:checkAgentStatus': { req: z.null(), res: z.object({ claude: z.object({ installed: z.boolean(), signedIn: z.boolean() }), codex: z.object({ installed: z.boolean(), signedIn: z.boolean() }), }), }, // Download + install an agent's native engine (the Settings "Enable" action). // Streams progress over the 'codeMode:engineProgress' push channel while it runs. 'codeMode:provisionEngine': { req: z.object({ agent: z.enum(['claude', 'codex']) }), res: z.object({ success: z.boolean(), error: z.string().optional() }), }, // Push (main -> renderer): engine provisioning progress for the Settings UI. 'codeMode:engineProgress': { req: z.object({ agent: z.enum(['claude', 'codex']), phase: z.enum(['download', 'verify', 'extract', 'done']), receivedBytes: z.number().optional(), totalBytes: z.number().optional(), }), res: z.null(), }, // ========================================================================== // Code section: project registry + coding sessions // ========================================================================== 'codeProject:add': { req: z.object({ path: z.string(), }), res: z.object({ project: CodeProject, git: GitRepoInfo, }), }, 'codeProject:remove': { req: z.object({ projectId: z.string(), }), res: z.object({ success: z.literal(true), }), }, 'codeProject:list': { req: z.null(), res: z.object({ projects: z.array(z.object({ project: CodeProject, git: GitRepoInfo, })), }), }, 'codeSession:create': { req: z.object({ projectId: z.string(), title: z.string().optional(), agent: CodingAgent, mode: CodeSessionMode, policy: ApprovalPolicy, isolation: z.enum(['in-repo', 'worktree']), // LLM for Rowboat-mode turns. Unset = the configured default. Like any // chat, the model is fixed once the session's run exists. model: z.string().optional(), provider: z.string().optional(), // The coding agent's own model + reasoning effort (ACP engine). Unlike the // Rowboat model these are re-applied each turn, so they stay editable. agentModel: z.string().optional(), agentEffort: z.string().optional(), }), res: z.object({ session: CodeSession, }), }, 'codeSession:list': { req: z.null(), res: z.object({ sessions: z.array(CodeSession), statuses: z.record(z.string(), CodeSessionStatus), }), }, 'codeSession:update': { req: z.object({ sessionId: z.string(), patch: CodeSession.pick({ title: true, mode: true, policy: true, agent: true, agentModel: true, agentEffort: true }).partial(), }), res: z.object({ session: CodeSession, }), }, // Live model + effort choices for a coding agent, discovered from the engine // (cached per agent in the main process). Mirrors what `/model` would show. 'codeMode:listModelOptions': { req: z.object({ agent: CodingAgent }), res: CodeAgentModelOptions, }, 'codeSession:delete': { req: z.object({ sessionId: z.string(), removeWorktree: z.boolean().optional(), deleteBranch: z.boolean().optional(), }), res: z.object({ success: z.literal(true), }), }, // Direct-drive: send the user's message straight to the session's ACP agent // (no copilot LLM in between). Streams back over `runs:events`. 'codeSession:sendMessage': { req: z.object({ sessionId: z.string(), text: z.string().min(1), }), res: z.object({ accepted: z.boolean(), error: z.string().optional(), }), }, 'codeSession:stop': { req: z.object({ sessionId: z.string(), }), res: z.object({ success: z.literal(true), }), }, 'codeSession:gitStatus': { req: z.object({ sessionId: z.string(), }), res: z.object({ isRepo: z.boolean(), branch: z.string().nullable(), hasCommits: z.boolean(), files: z.array(GitStatusFile), }), }, 'codeSession:fileDiff': { req: z.object({ sessionId: z.string(), path: z.string(), }), res: z.object({ oldText: z.string(), newText: z.string(), isBinary: z.boolean(), tooLarge: z.boolean(), }), }, 'codeSession:readdir': { req: z.object({ sessionId: z.string(), relPath: z.string(), }), res: z.object({ entries: z.array(z.object({ name: z.string(), kind: z.enum(['file', 'dir']), size: z.number().optional(), })), }), }, 'codeSession:readFile': { req: z.object({ sessionId: z.string(), relPath: z.string(), }), res: z.object({ content: z.string(), isBinary: z.boolean(), tooLarge: z.boolean(), }), }, 'codeSession:mergeBack': { req: z.object({ sessionId: z.string(), }), res: z.object({ ok: z.boolean(), conflict: z.boolean().optional(), message: z.string(), }), }, 'codeSession:cleanupWorktree': { req: z.object({ sessionId: z.string(), deleteBranch: z.boolean(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, // main → renderer: live session status transitions from the status tracker. 'codeSession:status': { req: z.object({ sessionId: z.string(), status: CodeSessionStatus, }), res: z.null(), }, // ========================================================================== // Embedded terminal (Code section): one PTY per coding session // ========================================================================== // Create-or-attach. Returns the scrollback backlog so a remounted view can // repaint what happened while it was closed. 'terminal:ensure': { req: z.object({ id: z.string(), cwd: z.string(), cols: z.number().int().positive(), rows: z.number().int().positive(), }), res: z.object({ backlog: z.string(), running: z.boolean(), }), }, 'terminal:input': { req: z.object({ id: z.string(), data: z.string(), }), res: z.object({ success: z.literal(true) }), }, 'terminal:resize': { req: z.object({ id: z.string(), cols: z.number().int().positive(), rows: z.number().int().positive(), }), res: z.object({ success: z.literal(true) }), }, 'terminal:dispose': { req: z.object({ id: z.string() }), res: z.object({ success: z.literal(true) }), }, // main → renderer streams 'terminal:data': { req: z.object({ id: z.string(), data: z.string() }), res: z.null(), }, 'terminal:exit': { req: z.object({ id: z.string(), exitCode: z.number() }), res: z.null(), }, 'granola:setConfig': { req: z.object({ enabled: z.boolean(), }), res: z.object({ success: z.literal(true), }), }, 'slack:getConfig': { req: z.null(), res: z.object({ enabled: z.boolean(), workspaces: z.array(z.object({ url: z.string(), name: z.string() })), }), }, 'slack:setConfig': { req: z.object({ enabled: z.boolean(), workspaces: z.array(z.object({ url: z.string(), name: z.string() })), }), res: z.object({ success: z.literal(true), }), }, 'slack:cliStatus': { req: z.null(), res: z.object({ available: z.boolean(), version: z.string().optional(), source: z.enum(['bundled', 'global', 'path']).optional(), }), }, 'slack:listWorkspaces': { req: z.null(), res: z.object({ workspaces: z.array(z.object({ url: z.string(), name: z.string() })), error: z.string().optional(), errorKind: SlackErrorKindSchema.optional(), }), }, 'slack:importDesktopAuth': { req: z.null(), res: z.object({ ok: z.boolean(), workspaces: z.array(z.object({ url: z.string(), name: z.string() })), error: z.string().optional(), errorKind: SlackErrorKindSchema.optional(), }), }, 'slack:quitAndImportDesktop': { req: z.null(), res: z.object({ ok: z.boolean(), workspaces: z.array(z.object({ url: z.string(), name: z.string() })), error: z.string().optional(), errorKind: SlackErrorKindSchema.optional(), }), }, 'slack:parseCurlAuth': { req: z.object({ curl: z.string() }), res: z.object({ ok: z.boolean(), workspaces: z.array(z.object({ url: z.string(), name: z.string() })), error: z.string().optional(), errorKind: SlackErrorKindSchema.optional(), }), }, 'slack:knowledgeStatus': { req: z.null(), res: z.object({ cli: z.object({ available: z.boolean(), version: z.string().optional(), source: z.enum(['bundled', 'global', 'path']).optional(), }), sources: z.array(z.object({ id: z.string(), enabled: z.boolean(), lastSyncAt: z.string().optional(), lastStatus: z.enum(['ok', 'error']).optional(), lastError: z.object({ kind: z.string(), message: z.string() }).optional(), nextDueAt: z.string().optional(), })), }), }, 'slack:listChannels': { req: z.object({ workspaceUrl: z.string(), }), res: z.object({ channels: z.array(z.object({ id: z.string(), name: z.string(), isPrivate: z.boolean().optional(), isMember: z.boolean().optional(), })), error: z.string().optional(), }), }, 'slack:getRecentMessages': { req: z.object({ limit: z.number().int().positive().max(20).optional(), }), res: z.object({ enabled: z.boolean(), messages: z.array(z.object({ id: z.string(), workspaceName: z.string().optional(), workspaceUrl: z.string().optional(), channelId: z.string().optional(), channelName: z.string().optional(), author: z.string().optional(), text: z.string(), ts: z.string(), url: z.string().optional(), })), error: z.string().optional(), errorKind: SlackErrorKindSchema.optional(), }), }, 'knowledgeSources:getConfig': { req: z.null(), res: z.object({ sources: z.array(KnowledgeSourceConfigSchema), }), }, 'knowledgeSources:upsert': { req: KnowledgeSourceConfigSchema, res: z.object({ sources: z.array(KnowledgeSourceConfigSchema), }), }, 'onboarding:getStatus': { req: z.null(), res: z.object({ showOnboarding: z.boolean(), }), }, 'onboarding:markComplete': { req: z.null(), res: z.object({ success: z.literal(true), }), }, // Composio integration channels 'composio:is-configured': { req: z.null(), res: z.object({ configured: z.boolean(), }), }, 'composio:set-api-key': { req: z.object({ apiKey: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'composio:initiate-connection': { req: z.object({ toolkitSlug: z.string(), }), res: z.object({ success: z.boolean(), redirectUrl: z.string().optional(), connectedAccountId: z.string().optional(), error: z.string().optional(), }), }, 'composio:get-connection-status': { req: z.object({ toolkitSlug: z.string(), }), res: z.object({ isConnected: z.boolean(), status: z.string().optional(), }), }, 'composio:sync-connection': { req: z.object({ toolkitSlug: z.string(), connectedAccountId: z.string(), }), res: z.object({ status: z.string(), }), }, 'composio:disconnect': { req: z.object({ toolkitSlug: z.string(), }), res: z.object({ success: z.boolean(), }), }, 'composio:list-connected': { req: z.null(), res: z.object({ toolkits: z.array(z.string()), }), }, 'migration:check-composio-google': { req: z.null(), res: z.object({ shouldShow: z.boolean(), }), }, 'composio:didConnect': { req: z.object({ toolkitSlug: z.string(), success: z.boolean(), error: z.string().optional(), }), res: z.null(), }, // Composio Tools Library channels 'composio:list-toolkits': { req: z.object({}), res: ZListToolkitsResponse, }, // Agent schedule channels 'agent-schedule:getConfig': { req: z.null(), res: AgentScheduleConfig, }, 'agent-schedule:getState': { req: z.null(), res: AgentScheduleState, }, 'agent-schedule:updateAgent': { req: z.object({ agentName: z.string(), entry: AgentScheduleEntry, }), res: z.object({ success: z.literal(true), }), }, 'agent-schedule:deleteAgent': { req: z.object({ agentName: z.string(), }), res: z.object({ success: z.literal(true), }), }, // Shell integration channels 'shell:openPath': { req: z.object({ path: z.string() }), res: z.object({ error: z.string().optional() }), }, 'shell:showItemInFolder': { req: z.object({ path: z.string() }), res: z.object({ success: z.literal(true) }), }, 'shell:readFileBase64': { req: z.object({ path: z.string() }), res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }), }, // Native dialog channels 'dialog:openDirectory': { req: z.object({ defaultPath: z.string().optional(), title: z.string().optional(), }), res: z.object({ path: z.string().nullable(), }), }, 'dialog:openFiles': { req: z.object({ defaultPath: z.string().optional(), title: z.string().optional(), }), res: z.object({ paths: z.array(z.string()), }), }, // Knowledge version history channels 'knowledge:history': { req: z.object({ path: RelPath }), res: z.object({ commits: z.array(z.object({ oid: z.string(), message: z.string(), timestamp: z.number(), author: z.string(), })), }), }, 'knowledge:fileAtCommit': { req: z.object({ path: RelPath, oid: z.string() }), res: z.object({ content: z.string() }), }, 'knowledge:restore': { req: z.object({ path: RelPath, oid: z.string() }), res: z.object({ ok: z.literal(true) }), }, 'knowledge:didCommit': { req: z.object({}), res: z.null(), }, // Google Docs linked knowledge files 'google-docs:getStatus': { req: z.null(), res: z.object({ connected: z.boolean(), hasRequiredScopes: z.boolean(), missingScopes: z.array(z.string()), }), }, 'google-docs:import': { req: z.object({ fileId: z.string().min(1), targetFolder: RelPath, }), res: z.object({ path: RelPath, doc: z.object({ id: z.string(), name: z.string(), url: z.string(), modifiedTime: z.string().nullable(), owner: z.string().nullable(), }), }), }, // Managed OAuth-redirect Picker: the Rowboat backend runs the pick with the // company Google client; the desktop opens the start URL, waits for the deep // link, and imports with the existing managed token. No API key or BYOK creds. 'google-docs:pickViaManaged': { req: z.object({ targetFolder: RelPath, }), res: z.object({ path: RelPath, doc: z.object({ id: z.string(), name: z.string(), url: z.string(), modifiedTime: z.string().nullable(), owner: z.string().nullable(), }), }).nullable(), }, 'google-docs:refreshSnapshot': { req: z.object({ path: RelPath, }), res: z.object({ ok: z.literal(true), syncedAt: z.string(), }), }, 'google-docs:sync': { req: z.object({ path: RelPath, // Overwrite the Google Doc even if it changed remotely since last sync. force: z.boolean().optional(), // Legacy field from the markdown-link path; ignored by the .docx sync. markdown: z.string().optional(), }), res: z.object({ synced: z.boolean(), syncedAt: z.string().optional(), // True when a remote edit was detected and the push was held back. conflict: z.boolean().optional(), error: z.string().optional(), }), }, // Is this local .docx linked to a Google Doc? Drives the sync UI in the viewer. 'google-docs:getLink': { req: z.object({ path: RelPath, }), res: z.object({ link: z.object({ id: z.string(), url: z.string(), title: z.string(), syncedAt: z.string(), remoteModifiedTime: z.string().optional(), }).nullable(), }), }, // Search channels 'search:query': { req: z.object({ query: z.string(), limit: z.number().optional(), types: z.array(z.enum(['knowledge', 'chat'])).optional(), }), res: z.object({ results: z.array(z.object({ type: z.enum(['knowledge', 'chat']), title: z.string(), preview: z.string(), path: z.string(), })), }), }, // Voice mode channels 'voice:getConfig': { req: z.null(), res: z.object({ deepgram: z.object({ apiKey: z.string() }).nullable(), elevenlabs: z.object({ apiKey: z.string(), voiceId: z.string().optional() }).nullable(), }), }, 'voice:synthesize': { req: z.object({ text: z.string(), }), res: z.object({ audioBase64: z.string(), mimeType: z.string(), }), }, // Ensures the OS-level microphone permission is settled before capturing. // On first-ever use (macOS) the permission is 'not-determined'; resolving // the native prompt up front prevents the in-flight getUserMedia from // rejecting on the first mic click. 'voice:ensureMicAccess': { req: z.null(), res: z.object({ granted: z.boolean(), }), }, // Same as ensureMicAccess but for the camera — settles the macOS TCC // permission before video mode calls getUserMedia({ video: true }). 'voice:ensureCameraAccess': { req: z.null(), res: z.object({ granted: z.boolean(), }), }, // Video-mode popout: show/hide the small always-on-top window (user + // mascot tiles) that floats over everything for the duration of a screen // share, Meet-style. 'video:setPopout': { req: z.object({ show: z.boolean() }), res: z.object({}), }, // Main-window renderer pushes the current call state; the main process // caches it and relays to the popout window (replayed on popout load). 'video:popoutState': { req: z.object({ ttsState: z.enum(['idle', 'synthesizing', 'speaking']), status: z.enum(['listening', 'thinking', 'speaking']).nullable(), cameraOn: z.boolean(), // Live transcript of the in-progress utterance (hands-free call only). interimText: z.string().nullable(), }), res: z.object({}), }, // Popout window → bring the main app window back to the foreground. 'video:focusMain': { req: z.null(), res: z.object({}), }, // Popout control bar → main process → relayed to the app window, which // executes the action on the live call. 'video:popoutAction': { req: z.object({ action: z.enum(['toggle-camera', 'stop-share', 'end-call']), }), res: z.object({}), }, // Push channel: main → popout window with the latest call state. 'video:popout-state': { req: z.object({ ttsState: z.enum(['idle', 'synthesizing', 'speaking']), status: z.enum(['listening', 'thinking', 'speaking']).nullable(), cameraOn: z.boolean(), interimText: z.string().nullable(), }), res: z.null(), }, // Push channel: main → app window with a popout control-bar action. 'video:popout-action': { req: z.object({ action: z.enum(['toggle-camera', 'stop-share', 'end-call']), }), res: z.null(), }, 'meeting:checkScreenPermission': { req: z.null(), res: z.object({ granted: z.boolean(), }), }, 'meeting:openScreenRecordingSettings': { req: z.null(), res: z.object({ success: z.boolean() }), }, 'meeting:summarize': { req: z.object({ transcript: z.string(), meetingStartTime: z.string().optional(), calendarEventJson: z.string().optional(), }), res: z.object({ notes: z.string(), }), }, // Resolve a meeting's attendees against the knowledge base — returns each // attendee's existing person note (or null). Deterministic, no LLM; powers // the ambient "Next up" prep card. 'meeting-prep:resolve': { req: z.object({ attendees: z.array(z.object({ email: z.string().optional(), displayName: z.string().optional(), self: z.boolean().optional(), })), // When provided, the response includes any pre-generated prep note for // this calendar event (matched by the eventId stamped in frontmatter). eventId: z.string().optional(), }), res: z.object({ attendees: z.array(z.object({ label: z.string(), email: z.string().optional(), displayName: z.string().optional(), note: z.object({ path: z.string(), name: z.string(), role: z.string().optional(), organization: z.string().optional(), markdown: z.string(), }).nullable(), })), organizations: z.array(z.object({ path: z.string(), name: z.string(), markdown: z.string(), })), // The pre-generated prep note (brief + path), if one exists for eventId. prepNote: z.object({ path: z.string(), brief: z.string(), }).nullable(), matchedCount: z.number().int().nonnegative(), unmatchedCount: z.number().int().nonnegative(), }), }, // Inline task schedule classification 'export:note': { req: z.object({ markdown: z.string(), format: z.enum(['md', 'pdf', 'docx']), title: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'inline-task:classifySchedule': { req: z.object({ instruction: z.string(), }), res: z.object({ schedule: z.union([ z.object({ type: z.literal('cron'), expression: z.string(), startDate: z.string(), endDate: z.string(), label: z.string() }), z.object({ type: z.literal('window'), cron: z.string(), startTime: z.string(), endTime: z.string(), startDate: z.string(), endDate: z.string(), label: z.string() }), z.object({ type: z.literal('once'), runAt: z.string(), label: z.string() }), ]).nullable(), }), }, 'inline-task:process': { req: z.object({ instruction: z.string(), noteContent: z.string(), notePath: z.string(), }), res: z.object({ instruction: z.string(), schedule: z.union([ z.object({ type: z.literal('cron'), expression: z.string(), startDate: z.string(), endDate: z.string() }), z.object({ type: z.literal('window'), cron: z.string(), startTime: z.string(), endTime: z.string(), startDate: z.string(), endDate: z.string() }), z.object({ type: z.literal('once'), runAt: z.string() }), ]).nullable(), scheduleLabel: z.string().nullable(), response: z.string().nullable(), }), }, // Live-note channels 'live-note:run': { req: z.object({ filePath: z.string(), context: z.string().optional(), }), res: z.object({ success: z.boolean(), runId: z.string().nullable().optional(), action: z.enum(['replace', 'no_update']).optional(), summary: z.string().nullable().optional(), contentAfter: z.string().nullable().optional(), error: z.string().optional(), }), }, 'live-note:get': { req: z.object({ filePath: z.string(), }), res: z.object({ success: z.boolean(), // Fresh, authoritative live-note object from frontmatter, or null when // the note is passive. Renderer should use this for display/edit — // never a stale cached copy. live: LiveNoteSchema.nullable().optional(), error: z.string().optional(), }), }, 'live-note:set': { req: z.object({ filePath: z.string(), live: LiveNoteSchema, }), res: z.object({ success: z.boolean(), live: LiveNoteSchema.nullable().optional(), error: z.string().optional(), }), }, 'live-note:setActive': { req: z.object({ filePath: z.string(), active: z.boolean(), }), res: z.object({ success: z.boolean(), live: LiveNoteSchema.nullable().optional(), error: z.string().optional(), }), }, 'live-note:delete': { req: z.object({ filePath: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'live-note:stop': { req: z.object({ filePath: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'live-note:listNotes': { req: z.null(), res: z.object({ notes: z.array(z.object({ path: RelPath, createdAt: z.string().nullable(), lastRunAt: z.string().nullable(), isActive: z.boolean(), objective: z.string(), })), }), }, // Background-task channels 'bg-task:run': { req: z.object({ slug: z.string(), context: z.string().optional(), }), res: z.object({ success: z.boolean(), runId: z.string().nullable().optional(), summary: z.string().nullable().optional(), error: z.string().optional(), }), }, 'bg-task:get': { req: z.object({ slug: z.string(), }), res: z.object({ success: z.boolean(), task: BackgroundTaskSchema.nullable().optional(), error: z.string().optional(), }), }, 'bg-task:patch': { req: z.object({ slug: z.string(), partial: BackgroundTaskSchema.partial(), }), res: z.object({ success: z.boolean(), task: BackgroundTaskSchema.nullable().optional(), error: z.string().optional(), }), }, 'bg-task:create': { req: z.object({ name: z.string(), instructions: z.string(), triggers: TriggersSchema.optional(), projectId: z.string().optional(), model: z.string().optional(), provider: z.string().optional(), }), res: z.object({ success: z.boolean(), slug: z.string().optional(), error: z.string().optional(), }), }, 'bg-task:delete': { req: z.object({ slug: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'bg-task:stop': { req: z.object({ slug: z.string(), }), res: z.object({ success: z.boolean(), error: z.string().optional(), }), }, 'bg-task:list': { req: z.object({ offset: z.number().int().nonnegative().optional(), limit: z.number().int().positive().optional(), sort: z.enum(['createdAt:desc', 'createdAt:asc', 'name:asc']).optional(), }), res: z.object({ items: z.array(BackgroundTaskSummarySchema), total: z.number().int().nonnegative(), }), }, // Returns the runIds recorded in `bg-tasks//runs.log` (newest first). // The renderer turns each id into a full Run via the existing `runs:fetch` // channel — bg-task transcripts now live at the global $WorkDir/runs/. 'bg-task:listRunIds': { req: z.object({ slug: z.string(), limit: z.number().int().positive().optional(), }), res: z.object({ runIds: z.array(z.string()), }), }, // Embedded browser (WebContentsView) channels 'browser:setBounds': { req: z.object({ x: z.number().int(), y: z.number().int(), width: z.number().int().nonnegative(), height: z.number().int().nonnegative(), }), res: z.object({ ok: z.literal(true) }), }, 'browser:setVisible': { req: z.object({ visible: z.boolean() }), res: z.object({ ok: z.literal(true) }), }, 'browser:newTab': { req: z.object({ url: z.string().min(1).refine( (u) => { const lower = u.trim().toLowerCase(); if (lower.startsWith('javascript:')) return false; if (lower.startsWith('file://')) return false; if (lower.startsWith('chrome://')) return false; if (lower.startsWith('chrome-extension://')) return false; return true; }, { message: 'Unsafe URL scheme' }, ).optional(), }), res: z.object({ ok: z.boolean(), tabId: z.string().optional(), error: z.string().optional(), }), }, 'browser:switchTab': { req: z.object({ tabId: z.string().min(1) }), res: z.object({ ok: z.boolean() }), }, 'browser:closeTab': { req: z.object({ tabId: z.string().min(1) }), res: z.object({ ok: z.boolean() }), }, 'browser:navigate': { req: z.object({ url: z.string().min(1).refine( (u) => { const lower = u.trim().toLowerCase(); if (lower.startsWith('javascript:')) return false; if (lower.startsWith('file://')) return false; if (lower.startsWith('chrome://')) return false; if (lower.startsWith('chrome-extension://')) return false; return true; }, { message: 'Unsafe URL scheme' }, ), }), res: z.object({ ok: z.boolean(), error: z.string().optional(), }), }, 'browser:back': { req: z.null(), res: z.object({ ok: z.boolean() }), }, 'browser:forward': { req: z.null(), res: z.object({ ok: z.boolean() }), }, 'browser:reload': { req: z.null(), res: z.object({ ok: z.literal(true) }), }, 'browser:getState': { req: z.null(), res: BrowserStateSchema, }, 'browser:didUpdateState': { req: BrowserStateSchema, res: z.null(), }, // HTTP basic/proxy auth challenge from a page in the embedded browser // (main → renderer push). The renderer shows a credential prompt and // answers via browser:httpAuthResponse. 'browser:httpAuthRequest': { req: HttpAuthRequestSchema, res: z.null(), }, // Main → renderer: a pending auth challenge was resolved without the // renderer answering (timed out, or its tab/window was destroyed), so the // renderer must drop the corresponding dialog from its queue. 'browser:httpAuthResolved': { req: z.object({ requestId: z.string() }), res: z.null(), }, // Renderer → main. Omit username to cancel the challenge; provide it (even // empty, for token-style auth) to submit credentials. 'browser:httpAuthResponse': { req: z.object({ requestId: z.string(), username: z.string().optional(), password: z.string().optional(), }), res: z.object({ ok: z.boolean() }), }, // Billing channels 'billing:getInfo': { req: z.null(), res: BillingInfoSchema, }, // Notification settings channels 'notifications:getSettings': { req: z.null(), res: NotificationSettingsSchema, }, 'notifications:setSettings': { req: NotificationSettingsSchema, res: z.object({ success: z.literal(true), }), }, } as const; // ============================================================================ // Type Helpers // ============================================================================ export type IPCChannels = { [K in keyof typeof ipcSchemas]: { req: z.infer; res: z.infer; }; }; /** * Channels that use invoke/handle (request/response pattern) * These are channels with non-null responses */ export type InvokeChannels = { [K in keyof IPCChannels]: IPCChannels[K]['res'] extends null ? never : K }[keyof IPCChannels]; /** * Channels that use send/on (fire-and-forget pattern) * These are channels with null responses (no response expected) */ export type SendChannels = { [K in keyof IPCChannels]: IPCChannels[K]['res'] extends null ? K : never }[keyof IPCChannels]; // ============================================================================ // Type Guards // ============================================================================ export function validateRequest( channel: K, data: unknown ): IPCChannels[K]['req'] { const schema = ipcSchemas[channel].req; return schema.parse(data) as IPCChannels[K]['req']; } export function validateResponse( channel: K, data: unknown ): IPCChannels[K]['res'] { const schema = ipcSchemas[channel].res; return schema.parse(data) as IPCChannels[K]['res']; }