rowboat/apps/x/packages/shared/src/ipc.ts
Tushar cccb7a8a65
Add plus button to prompt input for file and image attachments (#381)
* Add plus button to prompt input for file and image attachments

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor chat message attachment handling and improve UI for attachments in chat input and sidebar

* fixed review comments

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Arjun <6592213+arkml@users.noreply.github.com>
2026-02-26 21:41:17 +05:30

465 lines
10 KiB
TypeScript

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 } from './models.js';
import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js';
import { AgentScheduleState } from './agent-schedule-state.js';
import { ServiceEvent } from './service-events.js';
import { UserMessageContent } from './message.js';
// ============================================================================
// Runtime Validation Schemas (Single Source of Truth)
// ============================================================================
const ipcSchemas = {
'app:getVersions': {
req: z.null(),
res: z.object({
chrome: z.string(),
node: z.string(),
electron: 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(),
},
'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,
}),
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:delete': {
req: z.object({
runId: z.string(),
}),
res: z.object({ success: z.boolean() }),
},
'runs:events': {
req: z.null(),
res: z.null(),
},
'services:events': {
req: ServiceEvent,
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:saveConfig': {
req: LlmModelConfig,
res: z.object({
success: z.literal(true),
}),
},
'oauth:connect': {
req: z.object({
provider: z.string(),
clientId: 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(),
})),
}),
},
'oauth:didConnect': {
req: z.object({
provider: z.string(),
success: z.boolean(),
error: z.string().optional(),
}),
res: z.null(),
},
'granola:getConfig': {
req: z.null(),
res: z.object({
enabled: z.boolean(),
}),
},
'granola:setConfig': {
req: z.object({
enabled: z.boolean(),
}),
res: z.object({
success: z.literal(true),
}),
},
'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()),
}),
},
'composio:execute-action': {
req: z.object({
actionSlug: z.string(),
toolkitSlug: z.string(),
input: z.record(z.string(), z.unknown()),
}),
res: z.object({
success: z.boolean(),
data: z.unknown(),
error: z.string().optional(),
}),
},
'composio:didConnect': {
req: z.object({
toolkitSlug: z.string(),
success: z.boolean(),
error: z.string().optional(),
}),
res: z.null(),
},
// 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:readFileBase64': {
req: z.object({ path: z.string() }),
res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }),
},
// 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(),
})),
}),
},
} as const;
// ============================================================================
// Type Helpers
// ============================================================================
export type IPCChannels = {
[K in keyof typeof ipcSchemas]: {
req: z.infer<typeof ipcSchemas[K]['req']>;
res: z.infer<typeof ipcSchemas[K]['res']>;
};
};
/**
* 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<K extends keyof IPCChannels>(
channel: K,
data: unknown
): IPCChannels[K]['req'] {
const schema = ipcSchemas[channel].req;
return schema.parse(data) as IPCChannels[K]['req'];
}
export function validateResponse<K extends keyof IPCChannels>(
channel: K,
data: unknown
): IPCChannels[K]['res'] {
const schema = ipcSchemas[channel].res;
return schema.parse(data) as IPCChannels[K]['res'];
}