From 9ee2ba7b6e65f948f3c4d050070b03e71f2004f0 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 24 May 2026 12:36:48 -0400 Subject: [PATCH 1/2] Consolidate IPC types into shared module, enforce with `satisfies PearAPI` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the IPC surface had two competing definitions: preload/index.ts exported `typeof api` (the implementation) while renderer/src/lib/ipc.ts declared a separate, hand-maintained `PearAPI` interface that was wired into `window.pear`. Nothing forced them to agree — the preload's `invoke` calls happily returned a different shape than what the renderer's interface promised, and 13 burn/aiHist types were duplicated verbatim across the two files. This moves the entire IPC type surface to src/shared/types/ipc.ts: - preload/index.ts imports types from shared, tightens every `invoke` to the real return type, and ends with `satisfies PearAPI` so any future drift between implementation and interface fails to compile. - renderer/src/lib/ipc.ts becomes a thin re-export of @shared/types/ipc plus the `window.pear` global declaration — no duplicated types. All call sites continue to work unchanged (`import { ..., type Foo } from '@/lib/ipc'` still resolves). Type-check error count and test results match baseline. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/preload/index.ts | 441 +++++++++++--------- src/renderer/src/lib/ipc.ts | 640 +---------------------------- src/shared/types/ipc.ts | 781 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1043 insertions(+), 819 deletions(-) create mode 100644 src/shared/types/ipc.ts diff --git a/src/preload/index.ts b/src/preload/index.ts index a049d81..c11e812 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,74 +1,143 @@ import { contextBridge, ipcRenderer } from 'electron' +import type { + AiHistEntry, + AiHistRecentOptions, + AiHistResumeEntry, + AiHistSession, + AiHistStats, + AiHistStatusResponse, + AuthLoginInput, + AuthStatus, + BrokerAttachTerminalInput, + BrokerAttachTerminalResult, + BrokerDetails, + BrokerEventRecord, + BrokerListAgent, + BrokerSendMessageInput, + BrokerSetTerminalModeResult, + BrokerSpawnAgentInput, + BrokerSpawnAgentResult, + BrokerStatusEvent, + BurnAgentBreakdown, + BurnAgentInput, + BurnAgentSummary, + BurnProjectBreakdown, + BurnProjectInput, + BurnSessionLookup, + CloudAgentBinding, + CloudAgentEvent, + CloudAgentRecord, + CloudAgentStatus, + ConnectedIntegration, + CreateCloudAgentInput, + FsDirEntry, + FsReadPreviewResult, + GitBranchInfo, + GitBranchSyncStatus, + GitCheckoutBranchOptions, + GitCommitDraft, + GitCommitSelectionInput, + GitFileStatus, + GitGenerateCommitMessageInput, + GitHistoryCommit, + GitSummary, + IntegrationAdapter, + IntegrationConnectSession, + IntegrationsEvent, + PearAPI, + PendingRelayMessage, + ProactiveAgentBinding, + ProactiveAgentDeployResult, + ProactiveAgentDraft, + ProactiveAgentEvent, + ProactiveAgentRunsOptions, + ProactiveAgentRunsPage, + ProactiveAgentTranscript, + ProjectListResult, + TerminalAttachMode +} from '../shared/types/ipc' -export type ViewMode = 'terminal' | 'chat' | 'graph' | 'project-settings' | 'account-settings' | 'broker-details' | 'source-control' | 'ai-hist' | 'burn-session' - -type TerminalAttachMode = 'view' | 'drive' | 'passthrough' - -export type AiHistSource = 'claude' | 'codex' | 'cursor' | 'relay' - -export interface AiHistEntry { - id: number - source: AiHistSource - sessionId: string | null - project: string | null - prompt: string - timestampMs: number -} - -export interface AiHistSession { - sessionId: string - source: AiHistSource - project: string | null - firstPrompt: string - firstActivityMs: number - lastActivityMs: number - promptCount: number -} - -export interface AiHistStats { - total: number - bySource: Partial> - byProject: Array<{ project: string; count: number }> - firstTimestampMs: number | null - lastTimestampMs: number | null -} - -export interface BurnAgentInput { - projectId?: string - name: string - cwd?: string - cli?: string -} - -export interface BurnAgentSummary { - projectId?: string - name: string - agentKey: string - totalTokens: number - totalCost: number - turnCount: number - byModel: Array<{ model: string; tokens: number; cost: number }> - byTool: Array<{ tool: string; tokens: number; cost: number; count: number }> - sessionIds: Array<{ sessionId: string; ts?: string }> - updatedAt: number - status: 'ok' | 'unavailable' - error?: string -} - -export interface BurnAgentBreakdown extends BurnAgentSummary { - primarySessionId?: string - hotspots?: { - sessionId?: string - grandTotal: number - attributedTotal: number - unattributedTotal: number - attributionDegraded: boolean - files: Array<{ path: string; initialTokens: number; persistenceTokens: number; ridingTurns: number; totalCost: number }> - bashVerbs: Array<{ verb: string; callCount: number; distinctCommands: number; initialTokens: number; persistenceTokens: number; avgPersistenceTurns: number; totalCost: number; topExamples: string[] }> - bash: Array<{ command?: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> - subagents: Array<{ subagentType: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> - } -} +export type { + AiHistEntry, + AiHistRecentOptions, + AiHistResumeEntry, + AiHistSession, + AiHistSource, + AiHistStats, + AiHistStatusResponse, + AgentCurrentState, + AuthLoginInput, + AuthStatus, + AuthUser, + BrokerAgentDetails, + BrokerAttachTerminalInput, + BrokerAttachTerminalResult, + BrokerDetails, + BrokerEventRecord, + BrokerListAgent, + BrokerSendMessageInput, + BrokerSetTerminalModeResult, + BrokerSpawnAgentInput, + BrokerSpawnAgentResult, + BrokerStatusEvent, + BurnAgentBreakdown, + BurnAgentInput, + BurnAgentSummary, + BurnProjectAgentRollup, + BurnProjectBreakdown, + BurnProjectInput, + BurnSessionAgentRef, + BurnSessionLookup, + CloudAgentBinding, + CloudAgentEvent, + CloudAgentMountStatus, + CloudAgentRecord, + CloudAgentSandboxStatus, + CloudAgentStatus, + CloudAgentSyncMode, + ConnectedIntegration, + CreateCloudAgentInput, + FsDirEntry, + FsReadPreviewResult, + GitBranchInfo, + GitBranchSyncStatus, + GitCheckoutBranchOptions, + GitCommitDraft, + GitCommitSelectionInput, + GitFileStatus, + GitFileStatusKind, + GitGenerateCommitMessageInput, + GitHistoryCoAuthor, + GitHistoryCommit, + GitHistoryFile, + GitSummary, + InboundDeliveryMode, + IntegrationAdapter, + IntegrationAuthMethod, + IntegrationCapabilities, + IntegrationConnectSession, + IntegrationConnectStatus, + IntegrationsEvent, + MessageInjectionMode, + PearAPI, + PendingRelayMessage, + ProactiveAgentBinding, + ProactiveAgentDeployResult, + ProactiveAgentDraft, + ProactiveAgentEvent, + ProactiveAgentHarness, + ProactiveAgentRun, + ProactiveAgentRunMode, + ProactiveAgentRunStatus, + ProactiveAgentRunsOptions, + ProactiveAgentRunsPage, + ProactiveAgentStatus, + ProactiveAgentTranscript, + ProactiveAgentWatchEventKind, + ProjectListResult, + TerminalAttachMode, + ViewMode +} from '../shared/types/ipc' // Thin generic wrappers so each handler binds an IPC channel + return type without // repeating the `as Promise` cast on every call site. @@ -84,10 +153,10 @@ function subscribe(channel: string, callback: (payload: T) => void): () => vo const api = { app: { - confirmQuit: () => ipcRenderer.invoke('app:confirm-quit') as Promise + confirmQuit: () => invoke('app:confirm-quit') }, project: { - list: () => invoke<{ projects: unknown[]; activeId: string | null }>('project:list'), + list: () => invoke('project:list'), add: (name: string, rootPath?: string) => invoke('project:add', name, rootPath), remove: (id: string) => invoke('project:remove', id), setActive: (id: string | null) => invoke('project:set-active', id), @@ -119,35 +188,32 @@ const api = { name: string, channels?: string[], errorMessage?: string - ) => invoke<{ removed: string[] }>('broker:auto-fix-runtime', projectId, cwd, name, channels, errorMessage), + ) => + invoke<{ removed: string[] }>( + 'broker:auto-fix-runtime', + projectId, + cwd, + name, + channels, + errorMessage + ), connectCloud: () => invoke('broker:connect-cloud'), - spawnAgent: (projectId: string, input: { - name: string - cli: string - model?: string - task?: string - channels?: string[] - cwd?: string - args?: string[] - }) => invoke<{ name: string; runtime: string }>('broker:spawn-agent', projectId, input), - attachTerminal: (input: { - projectId?: string - name: string - rows?: number - cols?: number - mode?: TerminalAttachMode - }) => invoke('broker:attach-terminal', input), - sendInputFast: (projectId: string | undefined, name: string, data: string) => - ipcRenderer.send('broker:send-input-fast', projectId, name, data), + spawnAgent: (projectId: string, input: BrokerSpawnAgentInput) => + invoke('broker:spawn-agent', projectId, input), + attachTerminal: (input: BrokerAttachTerminalInput) => + invoke('broker:attach-terminal', input), + sendInputFast: (projectId: string | undefined, name: string, data: string): void => { + ipcRenderer.send('broker:send-input-fast', projectId, name, data) + }, setTerminalMode: (projectId: string | undefined, name: string, mode: TerminalAttachMode) => - invoke('broker:set-terminal-mode', projectId, name, mode), + invoke('broker:set-terminal-mode', projectId, name, mode), getPending: (projectId: string | undefined, name: string) => - invoke('broker:get-pending', projectId, name), + invoke('broker:get-pending', projectId, name), flushPending: (projectId: string | undefined, name: string) => invoke<{ flushed: number }>('broker:flush-pending', projectId, name), resizePty: (projectId: string | undefined, name: string, rows: number, cols: number) => invoke('broker:resize-pty', projectId, name, rows, cols), - sendMessage: (projectId: string | undefined, input: { to: string; text: string; from?: string }) => + sendMessage: (projectId: string | undefined, input: BrokerSendMessageInput) => invoke('broker:send-message', projectId, input), subscribeAgentChannel: (projectId: string | undefined, name: string, channel: string) => invoke('broker:subscribe-agent-channel', projectId, name, channel), @@ -155,9 +221,10 @@ const api = { invoke('broker:unsubscribe-agent-channel', projectId, name, channel), releaseAgent: (projectId: string | undefined, name: string) => invoke('broker:release-agent', projectId, name), - listAgents: (projectId?: string) => invoke('broker:list-agents', projectId), - listDetails: () => invoke('broker:list-details'), - listEvents: () => invoke('broker:list-events'), + listAgents: (projectId?: string) => + invoke('broker:list-agents', projectId), + listDetails: () => invoke('broker:list-details'), + listEvents: () => invoke('broker:list-events'), shutdown: () => invoke('broker:shutdown'), onEvent: (callback: (event: unknown) => void) => subscribe('broker:event', callback), onPtyChunk: (callback: (projectId: string, name: string, chunk: string) => void) => { @@ -166,145 +233,145 @@ const api = { ipcRenderer.on('broker:pty-chunk', handler) return () => ipcRenderer.removeListener('broker:pty-chunk', handler) }, - onStatus: (callback: (status: { projectId?: string; status: string; error?: string }) => void) => - subscribe<{ projectId?: string; status: string; error?: string }>('broker:status', callback) + onStatus: (callback: (status: BrokerStatusEvent) => void) => + subscribe('broker:status', callback) }, burn: { listAgentSummaries: (agents: BurnAgentInput[]) => invoke('burn:list-agent-summaries', agents), getAgentBreakdown: (agent: BurnAgentInput) => - invoke('burn:get-agent-breakdown', agent) + invoke('burn:get-agent-breakdown', agent), + getProjectBreakdown: (input: BurnProjectInput) => + invoke('burn:get-project-breakdown', input), + lookupSessions: (sessionIds: string[]) => + invoke>('burn:lookup-sessions', sessionIds) }, git: { - status: (path: string) => invoke('git:status', path), + status: (path: string) => invoke('git:status', path), diff: (path: string, file?: string) => invoke('git:diff', path, file), fileContent: (path: string, file: string, revision?: string) => invoke('git:file-content', path, file, revision), - summary: (path: string) => invoke('git:summary', path), + summary: (path: string) => invoke('git:summary', path), branches: (root: string) => invoke('git:branches', root), - branchDetails: (root: string) => invoke('git:branch-details', root), - checkoutBranch: (root: string, branch: string, options?: { stashChanges?: boolean }) => - invoke('git:checkout-branch', root, branch, options), - branchSyncStatus: (root: string) => invoke('git:branch-sync-status', root), - fetchRemote: (root: string) => invoke('git:fetch-remote', root), - pullCurrentBranch: (root: string) => invoke('git:pull-current-branch', root), - pushCurrentBranch: (root: string) => invoke('git:push-current-branch', root), - history: (path: string, limit?: number) => invoke('git:history', path, limit), - show: (path: string, hash: string, file?: string) => invoke('git:show', path, hash, file), - discardFiles: (path: string, files: string[]) => invoke('git:discard-files', path, files), + branchDetails: (root: string) => invoke('git:branch-details', root), + checkoutBranch: (root: string, branch: string, options?: GitCheckoutBranchOptions) => + invoke('git:checkout-branch', root, branch, options), + branchSyncStatus: (root: string) => invoke('git:branch-sync-status', root), + fetchRemote: (root: string) => invoke('git:fetch-remote', root), + pullCurrentBranch: (root: string) => + invoke('git:pull-current-branch', root), + pushCurrentBranch: (root: string) => + invoke('git:push-current-branch', root), + history: (path: string, limit?: number) => + invoke('git:history', path, limit), + show: (path: string, hash: string, file?: string) => + invoke('git:show', path, hash, file), + discardFiles: (path: string, files: string[]) => + invoke('git:discard-files', path, files), addGitignorePatterns: (path: string, patterns: string[]) => invoke('git:add-gitignore-patterns', path, patterns), - commitSelection: (path: string, input: { - title: string - body?: string - wholeFiles: string[] - patch?: string - }) => invoke<{ hash: string }>('git:commit-selection', path, input), - generateCommitMessage: (path: string, input: { wholeFiles: string[]; patch?: string }) => - invoke('git:generate-commit-message', path, input) + commitSelection: (path: string, input: GitCommitSelectionInput) => + invoke<{ hash: string }>('git:commit-selection', path, input), + generateCommitMessage: (path: string, input: GitGenerateCommitMessageInput) => + invoke('git:generate-commit-message', path, input) }, fs: { - listDir: (dirPath: string) => invoke('fs:list-dir', dirPath), - readPreview: (filePath: string) => invoke('fs:read-preview', filePath), + listDir: (dirPath: string) => invoke('fs:list-dir', dirPath), + readPreview: (filePath: string) => invoke('fs:read-preview', filePath), revealPath: (filePath: string) => invoke('fs:reveal-path', filePath) }, auth: { - login: (input?: { apiUrl?: string }) => invoke('auth:login', input), + login: (input?: AuthLoginInput) => invoke('auth:login', input), logout: () => invoke('auth:logout'), - status: () => invoke('auth:status') + status: () => invoke('auth:status') }, cloudAgent: { - list: () => ipcRenderer.invoke('cloud-agent:list'), - create: (input: { name: string; harness: string; model: string }) => - ipcRenderer.invoke('cloud-agent:create', input), - delete: (id: string) => ipcRenderer.invoke('cloud-agent:delete', id), + list: () => invoke('cloud-agent:list'), + create: (input: CreateCloudAgentInput) => + invoke('cloud-agent:create', input), + delete: (id: string) => invoke('cloud-agent:delete', id), attach: (projectId: string, cloudAgentId: string) => - ipcRenderer.invoke('cloud-agent:attach', projectId, cloudAgentId), - detach: (projectId: string) => ipcRenderer.invoke('cloud-agent:detach', projectId), - status: (projectId: string) => ipcRenderer.invoke('cloud-agent:status', projectId), - onEvent: (callback: (event: unknown) => void) => { - const handler = (_: unknown, event: unknown): void => callback(event) - ipcRenderer.on('cloud-agent:event', handler) - return () => ipcRenderer.removeListener('cloud-agent:event', handler) - } + invoke('cloud-agent:attach', projectId, cloudAgentId), + detach: (projectId: string) => invoke('cloud-agent:detach', projectId), + status: (projectId: string) => + invoke('cloud-agent:status', projectId), + onEvent: (callback: (event: CloudAgentEvent) => void) => + subscribe('cloud-agent:event', callback) }, proactiveAgent: { - list: (projectId: string) => ipcRenderer.invoke('proactive-agent:list', projectId), - create: (projectId: string, draft: unknown) => - ipcRenderer.invoke('proactive-agent:create', projectId, draft), - update: (projectId: string, personaId: string, draft: unknown) => - ipcRenderer.invoke('proactive-agent:update', projectId, personaId, draft), + list: (projectId: string) => + invoke('proactive-agent:list', projectId), + create: (projectId: string, draft: ProactiveAgentDraft) => + invoke('proactive-agent:create', projectId, draft), + update: (projectId: string, personaId: string, draft: ProactiveAgentDraft) => + invoke('proactive-agent:update', projectId, personaId, draft), deploy: (projectId: string, personaId: string) => - ipcRenderer.invoke('proactive-agent:deploy', projectId, personaId), + invoke('proactive-agent:deploy', projectId, personaId), pause: (projectId: string, personaId: string) => - ipcRenderer.invoke('proactive-agent:pause', projectId, personaId), + invoke('proactive-agent:pause', projectId, personaId), resume: (projectId: string, personaId: string) => - ipcRenderer.invoke('proactive-agent:resume', projectId, personaId), + invoke('proactive-agent:resume', projectId, personaId), undeploy: (projectId: string, personaId: string) => - ipcRenderer.invoke('proactive-agent:undeploy', projectId, personaId), - runs: (projectId: string, personaId: string, opts?: { limit?: number; cursor?: string }) => - ipcRenderer.invoke('proactive-agent:runs', projectId, personaId, opts), + invoke('proactive-agent:undeploy', projectId, personaId), + runs: (projectId: string, personaId: string, opts?: ProactiveAgentRunsOptions) => + invoke('proactive-agent:runs', projectId, personaId, opts), runTranscript: (runId: string) => - ipcRenderer.invoke('proactive-agent:run-transcript', runId), - onEvent: (callback: (event: unknown) => void) => { - const handler = (_: unknown, event: unknown): void => callback(event) - ipcRenderer.on('proactive-agent:event', handler) - return () => ipcRenderer.removeListener('proactive-agent:event', handler) - } + invoke('proactive-agent:run-transcript', runId), + onEvent: (callback: (event: ProactiveAgentEvent) => void) => + subscribe('proactive-agent:event', callback) }, integrations: { - catalog: () => ipcRenderer.invoke('integrations:catalog'), - list: (projectId: string) => ipcRenderer.invoke('integrations:list', projectId), + catalog: () => invoke('integrations:catalog'), + list: (projectId: string) => invoke('integrations:list', projectId), startConnect: (projectId: string, provider: string) => - ipcRenderer.invoke('integrations:start-connect', projectId, provider), + invoke('integrations:start-connect', projectId, provider), pollConnect: (sessionId: string) => - ipcRenderer.invoke('integrations:poll-connect', sessionId), + invoke('integrations:poll-connect', sessionId), completeConnect: ( projectId: string, sessionId: string, scope: Record, mountPaths: string[], notifyAgent: boolean - ) => ipcRenderer.invoke( - 'integrations:complete-connect', - projectId, - sessionId, - scope, - mountPaths, - notifyAgent - ), + ) => + invoke( + 'integrations:complete-connect', + projectId, + sessionId, + scope, + mountPaths, + notifyAgent + ), updateScope: ( projectId: string, integrationId: string, scope: Record, mountPaths: string[] - ) => ipcRenderer.invoke('integrations:update-scope', projectId, integrationId, scope, mountPaths), + ) => + invoke( + 'integrations:update-scope', + projectId, + integrationId, + scope, + mountPaths + ), disconnect: (projectId: string, integrationId: string) => - ipcRenderer.invoke('integrations:disconnect', projectId, integrationId), - onEvent: (callback: (event: unknown) => void) => { - const handler = (_: unknown, event: unknown): void => callback(event) - ipcRenderer.on('integrations:event', handler) - return () => ipcRenderer.removeListener('integrations:event', handler) - } + invoke('integrations:disconnect', projectId, integrationId), + onEvent: (callback: (event: IntegrationsEvent) => void) => + subscribe('integrations:event', callback) }, aiHist: { - status: () => - invoke<{ ok: true; dbPath: string } | { ok: false; reason: string }>('ai-hist:status'), - recent: (opts?: { source?: string; project?: string; limit?: number; beforeMs?: number }) => - invoke('ai-hist:recent', opts), - listSessions: (opts?: { source?: string; project?: string; limit?: number; beforeMs?: number }) => + status: () => invoke('ai-hist:status'), + recent: (opts?: AiHistRecentOptions) => invoke('ai-hist:recent', opts), + listSessions: (opts?: AiHistRecentOptions) => invoke('ai-hist:list-sessions', opts), getSession: (sessionId: string) => invoke('ai-hist:get-session', sessionId), - search: ( - query: string, - opts?: { source?: string; project?: string; limit?: number; beforeMs?: number } - ) => invoke('ai-hist:search', query, opts), - searchSessions: ( - query: string, - opts?: { source?: string; project?: string; limit?: number; beforeMs?: number } - ) => invoke('ai-hist:search-sessions', query, opts), + search: (query: string, opts?: AiHistRecentOptions) => + invoke('ai-hist:search', query, opts), + searchSessions: (query: string, opts?: AiHistRecentOptions) => + invoke('ai-hist:search-sessions', query, opts), stats: () => invoke('ai-hist:stats'), - resumeCommand: (entry: { source: string; sessionId: string | null; project: string | null }) => + resumeCommand: (entry: AiHistResumeEntry) => invoke('ai-hist:resume-command', entry), reload: () => invoke('ai-hist:reload') }, @@ -313,8 +380,6 @@ const api = { ipcRenderer.on(channel, handler) return () => ipcRenderer.removeListener(channel, handler) } -} +} satisfies PearAPI contextBridge.exposeInMainWorld('pear', api) - -export type PearAPI = typeof api diff --git a/src/renderer/src/lib/ipc.ts b/src/renderer/src/lib/ipc.ts index fcbd7dc..cefeac8 100644 --- a/src/renderer/src/lib/ipc.ts +++ b/src/renderer/src/lib/ipc.ts @@ -1,636 +1,14 @@ -export type TerminalAttachMode = 'view' | 'drive' | 'passthrough' -export type InboundDeliveryMode = 'auto_inject' | 'manual_flush' -export type MessageInjectionMode = 'wait' | 'steer' -export type AgentCurrentState = 'working' | 'idle' | 'blocked_on_send' +// Renderer-facing IPC entry point. +// +// All IPC type definitions live in @shared/types/ipc (a single source of +// truth shared with the preload, which uses `satisfies PearAPI` to enforce +// the implementation matches). We re-export everything from there so +// existing `import { ..., type Foo } from '@/lib/ipc'` call sites keep +// working unchanged. -export interface BurnAgentInput { - projectId?: string - name: string - cwd?: string - cli?: string -} - -export interface BurnAgentSummary { - projectId?: string - name: string - agentKey: string - totalTokens: number - totalCost: number - turnCount: number - byModel: Array<{ model: string; tokens: number; cost: number }> - byTool: Array<{ tool: string; tokens: number; cost: number; count: number }> - sessionIds: Array<{ sessionId: string; ts?: string }> - updatedAt: number - status: 'ok' | 'unavailable' - error?: string -} - -export interface BurnAgentBreakdown extends BurnAgentSummary { - primarySessionId?: string - hotspots?: { - sessionId?: string - grandTotal: number - attributedTotal: number - unattributedTotal: number - attributionDegraded: boolean - files: Array<{ path: string; initialTokens: number; persistenceTokens: number; ridingTurns: number; totalCost: number }> - bashVerbs: Array<{ verb: string; callCount: number; distinctCommands: number; initialTokens: number; persistenceTokens: number; avgPersistenceTurns: number; totalCost: number; topExamples: string[] }> - bash: Array<{ command?: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> - subagents: Array<{ subagentType: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> - } -} - -export interface PendingRelayMessage { - from: string - body: string - target: string - thread_id?: string - project_id?: string - project_alias?: string - priority: number - mode: MessageInjectionMode - queued_at_ms: number - event_id?: string -} - -export interface BrokerListAgent { - name: string - projectId: string - runtime?: string - cli?: string - model?: string - channels?: string[] - parent?: string - pid?: number - last_activity_at?: string - last_activity_ms?: number - current_state?: AgentCurrentState - inboundDeliveryMode?: InboundDeliveryMode -} - -export interface BrokerAgentDetails { - name: string - runtime: string - cli?: string - model?: string - channels: string[] - parent?: string - pid?: number - currentState?: AgentCurrentState -} - -export interface BrokerDetails { - projectId: string - name: string - cwd: string - channels: string[] - kind: 'local' | 'cloud' - url?: string - port?: number - apiKey?: string - brokerPid?: number - cloudSandboxId?: string | null - connectionPath?: string - connectionFileStatus?: 'matches' | 'missing' | 'different' | 'invalid' - apiKeyAvailable: boolean - health: 'connected' | 'unreachable' - session?: { - brokerVersion: string - protocolVersion: number - workspaceKey?: string - defaultWorkspaceId?: string - mode: string - uptimeSecs: number - } - relaycast?: { - workspaceKey?: string - defaultWorkspaceId?: string - authenticated?: boolean - workspaceCount?: number - workspaces: Array<{ - workspaceId: string - workspaceAlias?: string | null - selfName: string - selfAgentId: string - authenticated: boolean - default: boolean - }> - } - agentCount: number - pendingDeliveryCount: number - agents: BrokerAgentDetails[] - error?: string -} - -export interface BrokerEventRecord { - id: string - projectId: string - timestamp: number - event: Record & { - kind?: string - projectId?: string - } -} - -export type GitFileStatusKind = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' - -export interface GitFileStatus { - path: string - oldPath?: string - status: GitFileStatusKind - staged: boolean -} - -export interface GitSummary { - branch: string - additions: number - deletions: number -} - -export interface GitHistoryFile { - path: string - oldPath?: string - status: string -} - -export interface GitHistoryCoAuthor { - name: string - email: string - avatarUrl?: string - cachedAvatarUrl?: string -} - -export interface GitHistoryCommit { - hash: string - shortHash: string - author: string - authorEmail: string - authorAvatarUrl?: string - authorCachedAvatarUrl?: string - coAuthors: GitHistoryCoAuthor[] - date: string - subject: string - body: string - tags: string[] - additions: number - deletions: number - files: GitHistoryFile[] -} - -export interface GitCommitDraft { - title: string - body: string -} - -export interface GitCommitSelectionInput { - title: string - body?: string - wholeFiles: string[] - patch?: string -} - -export interface GitBranchInfo { - name: string - current: boolean - remote: boolean - lastCommitDate: string - defaultBranch: boolean -} - -export interface GitBranchSyncStatus { - branch: string - remote: string | null - upstream: string | null - ahead: number - behind: number - hasRemote: boolean -} - -export interface GitCheckoutBranchOptions { - stashChanges?: boolean -} - -export type CloudAgentRecord = { - id: string - name: string - displayName?: string - harness: string - defaultModel: string - status: 'ready' | 'warming' | 'error' | 'stopped' - lastUsedAt?: string - lastError?: string - lastAuthenticatedAt?: string | null -} - -export type CreateCloudAgentInput = { - name: string - harness: string - model: string -} - -export type CloudAgentSandboxStatus = 'warming' | 'ready' | 'failed' | 'stopping' | 'stopped' - -export type CloudAgentBinding = { - projectId: string - cloudAgentId: string - sandboxId: string - relayfileMountPath: string - attachedAt: string -} - -export type CloudAgentMountStatus = { - ready: boolean - lastReconcileAt?: string - pendingWrites: number - conflicts: number -} - -export type CloudAgentSyncMode = 'sandbox-priority' | 'local-priority' - -export type CloudAgentStatus = { - binding: CloudAgentBinding - sandbox: { id: string; status: CloudAgentSandboxStatus } - mount: CloudAgentMountStatus - syncMode: CloudAgentSyncMode -} - -export type CloudAgentEvent = - | { type: 'sandbox-status'; projectId: string; status: CloudAgentSandboxStatus } - | { type: 'mount-status'; projectId: string; mount: CloudAgentMountStatus } - | { type: 'sync-mode-changed'; projectId: string; syncMode: CloudAgentSyncMode } - | { type: 'error'; projectId: string; message: string } - -export type ProactiveAgentHarness = 'claude' | 'codex' | 'opencode' -export type ProactiveAgentStatus = 'draft' | 'warming' | 'active' | 'paused' | 'error' -export type ProactiveAgentRunStatus = 'running' | 'succeeded' | 'failed' -export type ProactiveAgentRunMode = 'cloud' | 'local' -export type ProactiveAgentWatchEventKind = 'created' | 'updated' | 'deleted' - -export type ProactiveAgentDraft = { - id: string - name: string - description?: string - cloudAgentId: string - harness: ProactiveAgentHarness - model: string - systemPrompt: string - integrations: Record> - watch: Array<{ - paths: string[] - events: ProactiveAgentWatchEventKind[] - debounceMs?: number - match?: string - }> - handlerCode: string - inputs?: Record - memory?: { enabled: boolean; scopes?: Array<'workspace' | 'project' | 'persona'>; ttlDays?: number } - harnessSettings?: { reasoning?: 'low' | 'medium' | 'high'; timeoutSeconds?: number } - mount?: { enabled: boolean } - runMode?: ProactiveAgentRunMode -} - -export type ProactiveAgentBinding = { - projectId: string - personaId: string - cloudAgentId: string - status: ProactiveAgentStatus - lastError?: string - lastFiredAt?: string - createdAt: string - updatedAt: string - draft: ProactiveAgentDraft -} - -export type ProactiveAgentRun = { - runId: string - projectId: string - personaId: string - firedAt: string - trigger: { - type: 'relayfile-change' - path: string - eventKind: ProactiveAgentWatchEventKind - } - durationMs?: number - status: ProactiveAgentRunStatus - summary?: string - error?: string -} - -export type ProactiveAgentTranscript = { - runId: string - projectId?: string - personaId?: string - messages: Array<{ - role: 'system' | 'user' | 'assistant' | 'tool' - content: string - ts: string - }> -} - -export type ProactiveAgentRunsPage = { - runs: ProactiveAgentRun[] - nextCursor?: string -} - -export type ProactiveAgentDeployResult = { - status: 'active' | 'warming' | 'error' - error?: string -} - -export type ProactiveAgentEvent = - | { type: 'binding-updated'; projectId: string; personaId: string; binding: ProactiveAgentBinding } - | { type: 'binding-removed'; projectId: string; personaId: string } - | { type: 'run-started'; projectId: string; personaId: string; run: ProactiveAgentRun } - | { type: 'run-update'; projectId: string; personaId: string; runId: string; chunk: string } - | { type: 'run-finished'; projectId: string; personaId: string; run: ProactiveAgentRun } - -export type IntegrationAuthMethod = 'oauth' | 'token' | 'apikey' - -export type IntegrationCapabilities = { - webhook: boolean - poll: boolean - writeback: boolean -} +import type { PearAPI } from '@shared/types/ipc' -export type IntegrationAdapter = { - provider: string - displayName: string - iconUrl?: string - version: string - capabilities: IntegrationCapabilities - authMethod: IntegrationAuthMethod - requiredScopes?: string[] - defaultMountPaths: string[] - description: string -} - -export type ConnectedIntegration = { - provider: string - integrationId: string - scope: Record - mountPaths: string[] - connectedAt: string - notifyAgent: boolean - lastSyncAt?: string - lastError?: string -} - -export type IntegrationConnectStatus = - | 'pending' - | 'awaiting-user' - | 'choosing-scope' - | 'completed' - | 'error' - | 'expired' - -export type IntegrationConnectSession = { - sessionId: string - provider: string - status: IntegrationConnectStatus - authUrl?: string - scopeChoices?: Record - integrationId?: string - error?: string -} - -export type IntegrationsEvent = - | { type: 'session-update'; sessionId: string; session: IntegrationConnectSession } - | { type: 'integration-added'; projectId: string; integration: ConnectedIntegration } - | { type: 'integration-removed'; projectId: string; integrationId: string } - | { type: 'integration-error'; projectId: string; integrationId: string; message: string } - -export interface AuthUser { - name?: string - email?: string - githubUsername?: string - username?: string - avatarUrl?: string - cachedAvatarUrl?: string - organizationName?: string - projectName?: string -} - -export interface AuthStatus { - loggedIn: boolean - apiUrl?: string - user?: AuthUser -} - -export interface PearAPI { - app: { - confirmQuit: () => Promise - } - project: { - list: () => Promise<{ projects: unknown[]; activeId: string | null }> - add: (name: string, rootPath?: string) => Promise - remove: (id: string) => Promise - setActive: (id: string | null) => Promise - update: (id: string, update: Record) => Promise - addChannel: (projectId: string, name: string) => Promise - removeChannel: (projectId: string, name: string) => Promise - setChannelPeople: (projectId: string, channelName: string, people: string[]) => Promise - addRoot: (projectId: string, name?: string, rootPath?: string) => Promise - removeRoot: (projectId: string, rootId: string) => Promise - addIntegration: (projectId: string, name: string, type?: string) => Promise - removeIntegration: (projectId: string, integrationId: string) => Promise - } - broker: { - start: (projectId: string, cwd: string, name: string, channels?: string[]) => Promise - syncChannels: (projectId: string, channels: string[]) => Promise - autoFixRuntime: ( - projectId: string, - cwd: string, - name: string, - channels?: string[], - errorMessage?: string - ) => Promise<{ removed: string[] }> - connectCloud: () => Promise - spawnAgent: (projectId: string, input: { - name: string - cli: string - model?: string - task?: string - channels?: string[] - cwd?: string - args?: string[] - }) => Promise<{ name: string; runtime: string }> - attachTerminal: (input: { - projectId?: string - name: string - rows?: number - cols?: number - mode?: TerminalAttachMode - }) => Promise<{ - name: string - mode: InboundDeliveryMode - previousMode?: InboundDeliveryMode - pending: number - snapshot?: { - rows: number - cols: number - cursor: [number, number] - screen: string - } - }> - sendInputFast: (projectId: string | undefined, name: string, data: string) => void - setTerminalMode: (projectId: string | undefined, name: string, mode: TerminalAttachMode) => Promise<{ - name: string - mode: InboundDeliveryMode - flushed: number - pending: number - }> - getPending: (projectId: string | undefined, name: string) => Promise - flushPending: (projectId: string | undefined, name: string) => Promise<{ flushed: number }> - resizePty: (projectId: string | undefined, name: string, rows: number, cols: number) => Promise - sendMessage: (projectId: string | undefined, input: { to: string; text: string; from?: string }) => Promise - subscribeAgentChannel: (projectId: string | undefined, name: string, channel: string) => Promise - unsubscribeAgentChannel: (projectId: string | undefined, name: string, channel: string) => Promise - releaseAgent: (projectId: string | undefined, name: string) => Promise - listAgents: (projectId?: string) => Promise - listDetails: () => Promise - listEvents: () => Promise - shutdown: () => Promise - onEvent: (callback: (event: unknown) => void) => () => void - onPtyChunk: (callback: (projectId: string, name: string, chunk: string) => void) => () => void - onStatus: (callback: (status: { projectId?: string; status: string; error?: string }) => void) => () => void - } - burn: { - listAgentSummaries: (agents: BurnAgentInput[]) => Promise - getAgentBreakdown: (agent: BurnAgentInput) => Promise - } - git: { - status: (path: string) => Promise - diff: (path: string, file?: string) => Promise - fileContent: (path: string, file: string, revision?: string) => Promise - summary: (path: string) => Promise - branches: (root: string) => Promise - branchDetails: (root: string) => Promise - checkoutBranch: (root: string, branch: string, options?: GitCheckoutBranchOptions) => Promise - branchSyncStatus: (root: string) => Promise - fetchRemote: (root: string) => Promise - pullCurrentBranch: (root: string) => Promise - pushCurrentBranch: (root: string) => Promise - history: (path: string, limit?: number) => Promise - show: (path: string, hash: string, file?: string) => Promise - discardFiles: (path: string, files: string[]) => Promise - addGitignorePatterns: (path: string, patterns: string[]) => Promise - commitSelection: (path: string, input: GitCommitSelectionInput) => Promise<{ hash: string }> - generateCommitMessage: ( - path: string, - input: { wholeFiles: string[]; patch?: string } - ) => Promise - } - fs: { - listDir: (dirPath: string) => Promise< - { name: string; path: string; type: 'file' | 'directory' }[] - > - readPreview: (filePath: string) => Promise<{ - kind: 'text' | 'binary' | 'too-large' | 'missing' - content: string - size: number - }> - revealPath: (filePath: string) => Promise - } - auth: { - login: (input?: { apiUrl?: string }) => Promise - logout: () => Promise - status: () => Promise - } - cloudAgent: { - list: () => Promise - create: (input: CreateCloudAgentInput) => Promise - delete: (id: string) => Promise - attach: (projectId: string, cloudAgentId: string) => Promise - detach: (projectId: string) => Promise - status: (projectId: string) => Promise - onEvent: (callback: (event: CloudAgentEvent) => void) => () => void - } - proactiveAgent: { - list: (projectId: string) => Promise - create: (projectId: string, draft: ProactiveAgentDraft) => Promise - update: (projectId: string, personaId: string, draft: ProactiveAgentDraft) => Promise - deploy: (projectId: string, personaId: string) => Promise - pause: (projectId: string, personaId: string) => Promise - resume: (projectId: string, personaId: string) => Promise - undeploy: (projectId: string, personaId: string) => Promise - runs: ( - projectId: string, - personaId: string, - opts?: { limit?: number; cursor?: string } - ) => Promise - runTranscript: (runId: string) => Promise - onEvent: (callback: (event: ProactiveAgentEvent) => void) => () => void - } - integrations: { - catalog: () => Promise - list: (projectId: string) => Promise - startConnect: (projectId: string, provider: string) => Promise - pollConnect: (sessionId: string) => Promise - completeConnect: ( - projectId: string, - sessionId: string, - scope: Record, - mountPaths: string[], - notifyAgent: boolean - ) => Promise - updateScope: ( - projectId: string, - integrationId: string, - scope: Record, - mountPaths: string[] - ) => Promise - disconnect: (projectId: string, integrationId: string) => Promise - onEvent: (callback: (event: IntegrationsEvent) => void) => () => void - } - aiHist: { - status: () => Promise<{ ok: true; dbPath: string } | { ok: false; reason: string }> - recent: (opts?: { source?: string; project?: string; limit?: number; beforeMs?: number }) => Promise - listSessions: (opts?: { source?: string; project?: string; limit?: number; beforeMs?: number }) => Promise - getSession: (sessionId: string) => Promise - search: ( - query: string, - opts?: { source?: string; project?: string; limit?: number; beforeMs?: number } - ) => Promise - searchSessions: ( - query: string, - opts?: { source?: string; project?: string; limit?: number; beforeMs?: number } - ) => Promise - stats: () => Promise - resumeCommand: (entry: { source: string; sessionId: string | null; project: string | null }) => Promise - reload: () => Promise - } - onMenu: (channel: string, callback: (...args: unknown[]) => void) => () => void -} - -export type AiHistSource = 'claude' | 'codex' | 'cursor' | 'relay' - -export interface AiHistEntry { - id: number - source: AiHistSource - sessionId: string | null - project: string | null - prompt: string - timestampMs: number -} - -export interface AiHistSession { - sessionId: string - source: AiHistSource - project: string | null - firstPrompt: string - firstActivityMs: number - lastActivityMs: number - promptCount: number -} - -export interface AiHistStats { - total: number - bySource: Partial> - byProject: Array<{ project: string; count: number }> - firstTimestampMs: number | null - lastTimestampMs: number | null -} +export * from '@shared/types/ipc' declare global { interface Window { diff --git a/src/shared/types/ipc.ts b/src/shared/types/ipc.ts new file mode 100644 index 0000000..9c1ed91 --- /dev/null +++ b/src/shared/types/ipc.ts @@ -0,0 +1,781 @@ +// Shared IPC types — the single source of truth for what crosses the +// preload bridge. Both src/preload/index.ts (the implementation) and +// src/renderer/src/lib/ipc.ts (the consumer-facing re-export) import from +// here. The preload uses `satisfies PearAPI` to enforce that the +// implementation matches this surface. + +export type ViewMode = + | 'terminal' + | 'chat' + | 'graph' + | 'project-settings' + | 'account-settings' + | 'broker-details' + | 'source-control' + | 'ai-hist' + | 'burn-session' + +export type TerminalAttachMode = 'view' | 'drive' | 'passthrough' +export type InboundDeliveryMode = 'auto_inject' | 'manual_flush' +export type MessageInjectionMode = 'wait' | 'steer' +export type AgentCurrentState = 'working' | 'idle' | 'blocked_on_send' + +export type AiHistSource = 'claude' | 'codex' | 'cursor' | 'relay' + +export interface AiHistEntry { + id: number + source: AiHistSource + sessionId: string | null + project: string | null + prompt: string + timestampMs: number +} + +export interface AiHistSession { + sessionId: string + source: AiHistSource + project: string | null + firstPrompt: string + firstActivityMs: number + lastActivityMs: number + promptCount: number +} + +export interface AiHistStats { + total: number + bySource: Partial> + byProject: Array<{ project: string; count: number }> + firstTimestampMs: number | null + lastTimestampMs: number | null +} + +export interface BurnAgentInput { + projectId?: string + name: string + cwd?: string + cli?: string +} + +export interface BurnAgentSummary { + projectId?: string + name: string + agentKey: string + totalTokens: number + totalCost: number + turnCount: number + byModel: Array<{ model: string; tokens: number; cost: number }> + byTool: Array<{ tool: string; tokens: number; cost: number; count: number }> + sessionIds: Array<{ sessionId: string; ts?: string }> + updatedAt: number + status: 'ok' | 'unavailable' + error?: string +} + +export interface BurnAgentBreakdown extends BurnAgentSummary { + primarySessionId?: string + hotspots?: { + sessionId?: string + grandTotal: number + attributedTotal: number + unattributedTotal: number + attributionDegraded: boolean + files: Array<{ path: string; initialTokens: number; persistenceTokens: number; ridingTurns: number; totalCost: number }> + bashVerbs: Array<{ verb: string; callCount: number; distinctCommands: number; initialTokens: number; persistenceTokens: number; avgPersistenceTurns: number; totalCost: number; topExamples: string[] }> + bash: Array<{ command?: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> + subagents: Array<{ subagentType: string; callCount: number; initialTokens: number; persistenceTokens: number; totalCost: number }> + } +} + +export interface BurnSessionAgentRef { + projectId?: string + name: string + agentKey: string + cli?: string + cwd?: string +} + +export interface BurnSessionLookup { + sessionId: string + totalTokens: number + totalCost: number + turnCount: number + agent?: BurnSessionAgentRef + status: 'ok' | 'unavailable' + error?: string +} + +export interface BurnProjectInput { + projectId: string +} + +export interface BurnProjectAgentRollup { + name: string + agentKey: string + totalTokens: number + totalCost: number + turnCount: number + sessionCount: number + cli?: string + cwd?: string + lastTs?: string +} + +export interface BurnProjectBreakdown { + projectId: string + totalTokens: number + totalCost: number + turnCount: number + byModel: Array<{ model: string; tokens: number; cost: number }> + byTool: Array<{ tool: string; tokens: number; cost: number; count: number }> + byAgent: BurnProjectAgentRollup[] + sessionIds: Array<{ sessionId: string; ts?: string }> + updatedAt: number + status: 'ok' | 'unavailable' + error?: string +} + +export interface PendingRelayMessage { + from: string + body: string + target: string + thread_id?: string + project_id?: string + project_alias?: string + priority: number + mode: MessageInjectionMode + queued_at_ms: number + event_id?: string +} + +export interface BrokerListAgent { + name: string + projectId: string + runtime?: string + cli?: string + model?: string + channels?: string[] + parent?: string + pid?: number + last_activity_at?: string + last_activity_ms?: number + current_state?: AgentCurrentState + inboundDeliveryMode?: InboundDeliveryMode +} + +export interface BrokerAgentDetails { + name: string + runtime: string + cli?: string + model?: string + channels: string[] + parent?: string + pid?: number + currentState?: AgentCurrentState +} + +export interface BrokerDetails { + projectId: string + name: string + cwd: string + channels: string[] + kind: 'local' | 'cloud' + url?: string + port?: number + apiKey?: string + brokerPid?: number + cloudSandboxId?: string | null + connectionPath?: string + connectionFileStatus?: 'matches' | 'missing' | 'different' | 'invalid' + apiKeyAvailable: boolean + health: 'connected' | 'unreachable' + session?: { + brokerVersion: string + protocolVersion: number + workspaceKey?: string + defaultWorkspaceId?: string + mode: string + uptimeSecs: number + } + relaycast?: { + workspaceKey?: string + defaultWorkspaceId?: string + authenticated?: boolean + workspaceCount?: number + workspaces: Array<{ + workspaceId: string + workspaceAlias?: string | null + selfName: string + selfAgentId: string + authenticated: boolean + default: boolean + }> + } + agentCount: number + pendingDeliveryCount: number + agents: BrokerAgentDetails[] + error?: string +} + +export interface BrokerEventRecord { + id: string + projectId: string + timestamp: number + event: Record & { + kind?: string + projectId?: string + } +} + +export interface BrokerSpawnAgentInput { + name: string + cli: string + model?: string + task?: string + channels?: string[] + cwd?: string + args?: string[] +} + +export interface BrokerSpawnAgentResult { + name: string + runtime: string +} + +export interface BrokerAttachTerminalInput { + projectId?: string + name: string + rows?: number + cols?: number + mode?: TerminalAttachMode +} + +export interface BrokerAttachTerminalResult { + name: string + mode: InboundDeliveryMode + previousMode?: InboundDeliveryMode + pending: number + snapshot?: { + rows: number + cols: number + cursor: [number, number] + screen: string + } +} + +export interface BrokerSetTerminalModeResult { + name: string + mode: InboundDeliveryMode + flushed: number + pending: number +} + +export interface BrokerSendMessageInput { + to: string + text: string + from?: string +} + +export interface BrokerStatusEvent { + projectId?: string + status: string + error?: string +} + +export type GitFileStatusKind = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' + +export interface GitFileStatus { + path: string + oldPath?: string + status: GitFileStatusKind + staged: boolean +} + +export interface GitSummary { + branch: string + additions: number + deletions: number +} + +export interface GitHistoryFile { + path: string + oldPath?: string + status: string +} + +export interface GitHistoryCoAuthor { + name: string + email: string + avatarUrl?: string + cachedAvatarUrl?: string +} + +export interface GitHistoryCommit { + hash: string + shortHash: string + author: string + authorEmail: string + authorAvatarUrl?: string + authorCachedAvatarUrl?: string + coAuthors: GitHistoryCoAuthor[] + date: string + subject: string + body: string + tags: string[] + additions: number + deletions: number + files: GitHistoryFile[] +} + +export interface GitCommitDraft { + title: string + body: string +} + +export interface GitCommitSelectionInput { + title: string + body?: string + wholeFiles: string[] + patch?: string +} + +export interface GitGenerateCommitMessageInput { + wholeFiles: string[] + patch?: string +} + +export interface GitBranchInfo { + name: string + current: boolean + remote: boolean + lastCommitDate: string + defaultBranch: boolean +} + +export interface GitBranchSyncStatus { + branch: string + remote: string | null + upstream: string | null + ahead: number + behind: number + hasRemote: boolean +} + +export interface GitCheckoutBranchOptions { + stashChanges?: boolean +} + +export interface FsDirEntry { + name: string + path: string + type: 'file' | 'directory' +} + +export interface FsReadPreviewResult { + kind: 'text' | 'binary' | 'too-large' | 'missing' + content: string + size: number +} + +export type CloudAgentRecord = { + id: string + name: string + displayName?: string + harness: string + defaultModel: string + status: 'ready' | 'warming' | 'error' | 'stopped' + lastUsedAt?: string + lastError?: string + lastAuthenticatedAt?: string | null +} + +export type CreateCloudAgentInput = { + name: string + harness: string + model: string +} + +export type CloudAgentSandboxStatus = 'warming' | 'ready' | 'failed' | 'stopping' | 'stopped' + +export type CloudAgentBinding = { + projectId: string + cloudAgentId: string + sandboxId: string + relayfileMountPath: string + attachedAt: string +} + +export type CloudAgentMountStatus = { + ready: boolean + lastReconcileAt?: string + pendingWrites: number + conflicts: number +} + +export type CloudAgentSyncMode = 'sandbox-priority' | 'local-priority' + +export type CloudAgentStatus = { + binding: CloudAgentBinding + sandbox: { id: string; status: CloudAgentSandboxStatus } + mount: CloudAgentMountStatus + syncMode: CloudAgentSyncMode +} + +export type CloudAgentEvent = + | { type: 'sandbox-status'; projectId: string; status: CloudAgentSandboxStatus } + | { type: 'mount-status'; projectId: string; mount: CloudAgentMountStatus } + | { type: 'sync-mode-changed'; projectId: string; syncMode: CloudAgentSyncMode } + | { type: 'error'; projectId: string; message: string } + +export type ProactiveAgentHarness = 'claude' | 'codex' | 'opencode' +export type ProactiveAgentStatus = 'draft' | 'warming' | 'active' | 'paused' | 'error' +export type ProactiveAgentRunStatus = 'running' | 'succeeded' | 'failed' +export type ProactiveAgentRunMode = 'cloud' | 'local' +export type ProactiveAgentWatchEventKind = 'created' | 'updated' | 'deleted' + +export type ProactiveAgentDraft = { + id: string + name: string + description?: string + cloudAgentId: string + harness: ProactiveAgentHarness + model: string + systemPrompt: string + integrations: Record> + watch: Array<{ + paths: string[] + events: ProactiveAgentWatchEventKind[] + debounceMs?: number + match?: string + }> + handlerCode: string + inputs?: Record + memory?: { enabled: boolean; scopes?: Array<'workspace' | 'project' | 'persona'>; ttlDays?: number } + harnessSettings?: { reasoning?: 'low' | 'medium' | 'high'; timeoutSeconds?: number } + mount?: { enabled: boolean } + runMode?: ProactiveAgentRunMode +} + +export type ProactiveAgentBinding = { + projectId: string + personaId: string + cloudAgentId: string + status: ProactiveAgentStatus + lastError?: string + lastFiredAt?: string + createdAt: string + updatedAt: string + draft: ProactiveAgentDraft +} + +export type ProactiveAgentRun = { + runId: string + projectId: string + personaId: string + firedAt: string + trigger: { + type: 'relayfile-change' + path: string + eventKind: ProactiveAgentWatchEventKind + } + durationMs?: number + status: ProactiveAgentRunStatus + summary?: string + error?: string +} + +export type ProactiveAgentTranscript = { + runId: string + projectId?: string + personaId?: string + messages: Array<{ + role: 'system' | 'user' | 'assistant' | 'tool' + content: string + ts: string + }> +} + +export type ProactiveAgentRunsPage = { + runs: ProactiveAgentRun[] + nextCursor?: string +} + +export type ProactiveAgentDeployResult = { + status: 'active' | 'warming' | 'error' + error?: string +} + +export type ProactiveAgentRunsOptions = { + limit?: number + cursor?: string +} + +export type ProactiveAgentEvent = + | { type: 'binding-updated'; projectId: string; personaId: string; binding: ProactiveAgentBinding } + | { type: 'binding-removed'; projectId: string; personaId: string } + | { type: 'run-started'; projectId: string; personaId: string; run: ProactiveAgentRun } + | { type: 'run-update'; projectId: string; personaId: string; runId: string; chunk: string } + | { type: 'run-finished'; projectId: string; personaId: string; run: ProactiveAgentRun } + +export type IntegrationAuthMethod = 'oauth' | 'token' | 'apikey' + +export type IntegrationCapabilities = { + webhook: boolean + poll: boolean + writeback: boolean +} + +export type IntegrationAdapter = { + provider: string + displayName: string + iconUrl?: string + version: string + capabilities: IntegrationCapabilities + authMethod: IntegrationAuthMethod + requiredScopes?: string[] + defaultMountPaths: string[] + description: string +} + +export type ConnectedIntegration = { + provider: string + integrationId: string + scope: Record + mountPaths: string[] + connectedAt: string + notifyAgent: boolean + lastSyncAt?: string + lastError?: string +} + +export type IntegrationConnectStatus = + | 'pending' + | 'awaiting-user' + | 'choosing-scope' + | 'completed' + | 'error' + | 'expired' + +export type IntegrationConnectSession = { + sessionId: string + provider: string + status: IntegrationConnectStatus + authUrl?: string + scopeChoices?: Record + integrationId?: string + error?: string +} + +export type IntegrationsEvent = + | { type: 'session-update'; sessionId: string; session: IntegrationConnectSession } + | { type: 'integration-added'; projectId: string; integration: ConnectedIntegration } + | { type: 'integration-removed'; projectId: string; integrationId: string } + | { type: 'integration-error'; projectId: string; integrationId: string; message: string } + +export interface AuthUser { + name?: string + email?: string + githubUsername?: string + username?: string + avatarUrl?: string + cachedAvatarUrl?: string + organizationName?: string + projectName?: string +} + +export interface AuthStatus { + loggedIn: boolean + apiUrl?: string + user?: AuthUser +} + +export interface AuthLoginInput { + apiUrl?: string +} + +export interface AiHistRecentOptions { + source?: string + project?: string + limit?: number + beforeMs?: number +} + +export interface AiHistStatusResult { + ok: true + dbPath: string +} + +export interface AiHistStatusError { + ok: false + reason: string +} + +export type AiHistStatusResponse = AiHistStatusResult | AiHistStatusError + +export interface AiHistResumeEntry { + source: string + sessionId: string | null + project: string | null +} + +export interface ProjectListResult { + projects: unknown[] + activeId: string | null +} + +export interface PearAPI { + app: { + confirmQuit: () => Promise + } + project: { + list: () => Promise + add: (name: string, rootPath?: string) => Promise + remove: (id: string) => Promise + setActive: (id: string | null) => Promise + update: (id: string, update: Record) => Promise + addChannel: (projectId: string, name: string) => Promise + removeChannel: (projectId: string, name: string) => Promise + setChannelPeople: (projectId: string, channelName: string, people: string[]) => Promise + addRoot: (projectId: string, name?: string, rootPath?: string) => Promise + removeRoot: (projectId: string, rootId: string) => Promise + addIntegration: (projectId: string, name: string, type?: string) => Promise + removeIntegration: (projectId: string, integrationId: string) => Promise + } + broker: { + start: (projectId: string, cwd: string, name: string, channels?: string[]) => Promise + syncChannels: (projectId: string, channels: string[]) => Promise + autoFixRuntime: ( + projectId: string, + cwd: string, + name: string, + channels?: string[], + errorMessage?: string + ) => Promise<{ removed: string[] }> + connectCloud: () => Promise + spawnAgent: (projectId: string, input: BrokerSpawnAgentInput) => Promise + attachTerminal: (input: BrokerAttachTerminalInput) => Promise + sendInputFast: (projectId: string | undefined, name: string, data: string) => void + setTerminalMode: ( + projectId: string | undefined, + name: string, + mode: TerminalAttachMode + ) => Promise + getPending: (projectId: string | undefined, name: string) => Promise + flushPending: (projectId: string | undefined, name: string) => Promise<{ flushed: number }> + resizePty: (projectId: string | undefined, name: string, rows: number, cols: number) => Promise + sendMessage: (projectId: string | undefined, input: BrokerSendMessageInput) => Promise + subscribeAgentChannel: (projectId: string | undefined, name: string, channel: string) => Promise + unsubscribeAgentChannel: (projectId: string | undefined, name: string, channel: string) => Promise + releaseAgent: (projectId: string | undefined, name: string) => Promise + listAgents: (projectId?: string) => Promise + listDetails: () => Promise + listEvents: () => Promise + shutdown: () => Promise + onEvent: (callback: (event: unknown) => void) => () => void + onPtyChunk: (callback: (projectId: string, name: string, chunk: string) => void) => () => void + onStatus: (callback: (status: BrokerStatusEvent) => void) => () => void + } + burn: { + listAgentSummaries: (agents: BurnAgentInput[]) => Promise + getAgentBreakdown: (agent: BurnAgentInput) => Promise + getProjectBreakdown: (input: BurnProjectInput) => Promise + lookupSessions: (sessionIds: string[]) => Promise> + } + git: { + status: (path: string) => Promise + diff: (path: string, file?: string) => Promise + fileContent: (path: string, file: string, revision?: string) => Promise + summary: (path: string) => Promise + branches: (root: string) => Promise + branchDetails: (root: string) => Promise + checkoutBranch: ( + root: string, + branch: string, + options?: GitCheckoutBranchOptions + ) => Promise + branchSyncStatus: (root: string) => Promise + fetchRemote: (root: string) => Promise + pullCurrentBranch: (root: string) => Promise + pushCurrentBranch: (root: string) => Promise + history: (path: string, limit?: number) => Promise + show: (path: string, hash: string, file?: string) => Promise + discardFiles: (path: string, files: string[]) => Promise + addGitignorePatterns: (path: string, patterns: string[]) => Promise + commitSelection: (path: string, input: GitCommitSelectionInput) => Promise<{ hash: string }> + generateCommitMessage: ( + path: string, + input: GitGenerateCommitMessageInput + ) => Promise + } + fs: { + listDir: (dirPath: string) => Promise + readPreview: (filePath: string) => Promise + revealPath: (filePath: string) => Promise + } + auth: { + login: (input?: AuthLoginInput) => Promise + logout: () => Promise + status: () => Promise + } + cloudAgent: { + list: () => Promise + create: (input: CreateCloudAgentInput) => Promise + delete: (id: string) => Promise + attach: (projectId: string, cloudAgentId: string) => Promise + detach: (projectId: string) => Promise + status: (projectId: string) => Promise + onEvent: (callback: (event: CloudAgentEvent) => void) => () => void + } + proactiveAgent: { + list: (projectId: string) => Promise + create: (projectId: string, draft: ProactiveAgentDraft) => Promise + update: ( + projectId: string, + personaId: string, + draft: ProactiveAgentDraft + ) => Promise + deploy: (projectId: string, personaId: string) => Promise + pause: (projectId: string, personaId: string) => Promise + resume: (projectId: string, personaId: string) => Promise + undeploy: (projectId: string, personaId: string) => Promise + runs: ( + projectId: string, + personaId: string, + opts?: ProactiveAgentRunsOptions + ) => Promise + runTranscript: (runId: string) => Promise + onEvent: (callback: (event: ProactiveAgentEvent) => void) => () => void + } + integrations: { + catalog: () => Promise + list: (projectId: string) => Promise + startConnect: (projectId: string, provider: string) => Promise + pollConnect: (sessionId: string) => Promise + completeConnect: ( + projectId: string, + sessionId: string, + scope: Record, + mountPaths: string[], + notifyAgent: boolean + ) => Promise + updateScope: ( + projectId: string, + integrationId: string, + scope: Record, + mountPaths: string[] + ) => Promise + disconnect: (projectId: string, integrationId: string) => Promise + onEvent: (callback: (event: IntegrationsEvent) => void) => () => void + } + aiHist: { + status: () => Promise + recent: (opts?: AiHistRecentOptions) => Promise + listSessions: (opts?: AiHistRecentOptions) => Promise + getSession: (sessionId: string) => Promise + search: (query: string, opts?: AiHistRecentOptions) => Promise + searchSessions: (query: string, opts?: AiHistRecentOptions) => Promise + stats: () => Promise + resumeCommand: (entry: AiHistResumeEntry) => Promise + reload: () => Promise + } + onMenu: (channel: string, callback: (...args: unknown[]) => void) => () => void +} From f1bef2e853386758f5a024d4e340354fcf6a33ad Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 24 May 2026 12:44:05 -0400 Subject: [PATCH 2/2] Drop burn:get-project-breakdown and burn:lookup-sessions from preload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit on this branch picked up two preload methods (and their types) from the working tree that don't exist on main — the matching ipcMain handlers were sitting uncommitted alongside an in-progress burn project page. Shipping the PR alone would have exposed two channels with no handler, failing at runtime with "No handler registered." Removing both preload methods plus BurnProjectInput, BurnProjectBreakdown, BurnProjectAgentRollup, BurnSessionLookup, and BurnSessionAgentRef from shared/types/ipc.ts. The burn-project feature will re-add them in its own commit alongside the matching handlers. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/preload/index.ts | 14 +----------- src/shared/types/ipc.ts | 50 ----------------------------------------- 2 files changed, 1 insertion(+), 63 deletions(-) diff --git a/src/preload/index.ts b/src/preload/index.ts index c11e812..69fb66d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -21,9 +21,6 @@ import type { BurnAgentBreakdown, BurnAgentInput, BurnAgentSummary, - BurnProjectBreakdown, - BurnProjectInput, - BurnSessionLookup, CloudAgentBinding, CloudAgentEvent, CloudAgentRecord, @@ -83,11 +80,6 @@ export type { BurnAgentBreakdown, BurnAgentInput, BurnAgentSummary, - BurnProjectAgentRollup, - BurnProjectBreakdown, - BurnProjectInput, - BurnSessionAgentRef, - BurnSessionLookup, CloudAgentBinding, CloudAgentEvent, CloudAgentMountStatus, @@ -240,11 +232,7 @@ const api = { listAgentSummaries: (agents: BurnAgentInput[]) => invoke('burn:list-agent-summaries', agents), getAgentBreakdown: (agent: BurnAgentInput) => - invoke('burn:get-agent-breakdown', agent), - getProjectBreakdown: (input: BurnProjectInput) => - invoke('burn:get-project-breakdown', input), - lookupSessions: (sessionIds: string[]) => - invoke>('burn:lookup-sessions', sessionIds) + invoke('burn:get-agent-breakdown', agent) }, git: { status: (path: string) => invoke('git:status', path), diff --git a/src/shared/types/ipc.ts b/src/shared/types/ipc.ts index 9c1ed91..252a5f8 100644 --- a/src/shared/types/ipc.ts +++ b/src/shared/types/ipc.ts @@ -86,54 +86,6 @@ export interface BurnAgentBreakdown extends BurnAgentSummary { } } -export interface BurnSessionAgentRef { - projectId?: string - name: string - agentKey: string - cli?: string - cwd?: string -} - -export interface BurnSessionLookup { - sessionId: string - totalTokens: number - totalCost: number - turnCount: number - agent?: BurnSessionAgentRef - status: 'ok' | 'unavailable' - error?: string -} - -export interface BurnProjectInput { - projectId: string -} - -export interface BurnProjectAgentRollup { - name: string - agentKey: string - totalTokens: number - totalCost: number - turnCount: number - sessionCount: number - cli?: string - cwd?: string - lastTs?: string -} - -export interface BurnProjectBreakdown { - projectId: string - totalTokens: number - totalCost: number - turnCount: number - byModel: Array<{ model: string; tokens: number; cost: number }> - byTool: Array<{ tool: string; tokens: number; cost: number; count: number }> - byAgent: BurnProjectAgentRollup[] - sessionIds: Array<{ sessionId: string; ts?: string }> - updatedAt: number - status: 'ok' | 'unavailable' - error?: string -} - export interface PendingRelayMessage { from: string body: string @@ -677,8 +629,6 @@ export interface PearAPI { burn: { listAgentSummaries: (agents: BurnAgentInput[]) => Promise getAgentBreakdown: (agent: BurnAgentInput) => Promise - getProjectBreakdown: (input: BurnProjectInput) => Promise - lookupSessions: (sessionIds: string[]) => Promise> } git: { status: (path: string) => Promise