agent-ecosystem/src/preload/index.ts
iliya 1100d8fa3d refactor: streamline member handling and improve type usage
- Updated the ReplaceMembersRequest import in preload to use a local type definition for consistency.
- Refactored MemberDraftRow and MembersEditorSection components to utilize InlineChip type directly, enhancing type clarity.
- Improved performance by using useMemo for workflow chips in MemberDraftRow, optimizing re-renders.
- Adjusted the JSON text setting in MembersEditorSection to use queueMicrotask for better asynchronous handling.
- Added TEAM_REPLACE_MEMBERS to IPC mock definitions for improved testing coverage.
2026-03-03 01:12:28 +02:00

1070 lines
39 KiB
TypeScript

import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants';
import { contextBridge, ipcRenderer } from 'electron';
import {
APP_RELAUNCH,
CLI_INSTALLER_GET_STATUS,
CLI_INSTALLER_INSTALL,
CLI_INSTALLER_PROGRESS,
CONTEXT_CHANGED,
CONTEXT_GET_ACTIVE,
CONTEXT_LIST,
CONTEXT_SWITCH,
EDITOR_CHANGE,
EDITOR_CLOSE,
EDITOR_CREATE_DIR,
EDITOR_CREATE_FILE,
EDITOR_DELETE_FILE,
EDITOR_GIT_STATUS,
EDITOR_LIST_FILES,
EDITOR_MOVE_FILE,
EDITOR_OPEN,
EDITOR_READ_BINARY_PREVIEW,
EDITOR_READ_DIR,
EDITOR_READ_FILE,
EDITOR_RENAME_FILE,
EDITOR_SEARCH_IN_FILES,
EDITOR_SET_WATCHED_DIRS,
EDITOR_SET_WATCHED_FILES,
EDITOR_WATCH_DIR,
EDITOR_WRITE_FILE,
HTTP_SERVER_GET_STATUS,
HTTP_SERVER_START,
HTTP_SERVER_STOP,
PROJECT_LIST_FILES,
REVIEW_APPLY_DECISIONS,
REVIEW_CHECK_CONFLICT,
REVIEW_CLEAR_DECISIONS,
REVIEW_GET_AGENT_CHANGES,
REVIEW_GET_CHANGE_STATS,
REVIEW_GET_FILE_CONTENT,
REVIEW_GET_GIT_FILE_LOG,
REVIEW_GET_TASK_CHANGES,
REVIEW_LOAD_DECISIONS,
REVIEW_PREVIEW_REJECT,
REVIEW_REJECT_FILE,
REVIEW_REJECT_HUNKS,
REVIEW_SAVE_DECISIONS,
REVIEW_SAVE_EDITED_FILE,
SSH_CONNECT,
SSH_DISCONNECT,
SSH_GET_CONFIG_HOSTS,
SSH_GET_LAST_CONNECTION,
SSH_GET_STATE,
SSH_RESOLVE_HOST,
SSH_SAVE_LAST_CONNECTION,
SSH_STATUS,
SSH_TEST,
TEAM_ADD_MEMBER,
TEAM_ADD_TASK_COMMENT,
TEAM_ADD_TASK_RELATIONSHIP,
TEAM_ALIVE_LIST,
TEAM_CANCEL_PROVISIONING,
TEAM_CHANGE,
TEAM_CREATE,
TEAM_CREATE_CONFIG,
TEAM_CREATE_TASK,
TEAM_DELETE_TEAM,
TEAM_GET_ALL_TASKS,
TEAM_GET_ATTACHMENTS,
TEAM_GET_DATA,
TEAM_GET_DELETED_TASKS,
TEAM_GET_LOGS_FOR_TASK,
TEAM_GET_MEMBER_LOGS,
TEAM_GET_MEMBER_STATS,
TEAM_GET_PROJECT_BRANCH,
TEAM_KILL_PROCESS,
TEAM_LAUNCH,
TEAM_LEAD_ACTIVITY,
TEAM_LIST,
TEAM_PERMANENTLY_DELETE,
TEAM_PREPARE_PROVISIONING,
TEAM_PROCESS_ALIVE,
TEAM_PROCESS_SEND,
TEAM_PROVISIONING_PROGRESS,
TEAM_PROVISIONING_STATUS,
TEAM_REMOVE_MEMBER,
TEAM_REMOVE_TASK_RELATIONSHIP,
TEAM_REPLACE_MEMBERS,
TEAM_REQUEST_REVIEW,
TEAM_RESTORE,
TEAM_RESTORE_TASK,
TEAM_SEND_MESSAGE,
TEAM_SET_TASK_CLARIFICATION,
TEAM_SHOW_MESSAGE_NOTIFICATION,
TEAM_SOFT_DELETE_TASK,
TEAM_START_TASK,
TEAM_STOP,
TEAM_UPDATE_CONFIG,
TEAM_UPDATE_KANBAN,
TEAM_UPDATE_KANBAN_COLUMN_ORDER,
TEAM_UPDATE_MEMBER_ROLE,
TEAM_UPDATE_TASK_FIELDS,
TEAM_UPDATE_TASK_OWNER,
TEAM_UPDATE_TASK_STATUS,
TERMINAL_DATA,
TERMINAL_EXIT,
TERMINAL_KILL,
TERMINAL_RESIZE,
TERMINAL_SPAWN,
TERMINAL_WRITE,
UPDATER_CHECK,
UPDATER_DOWNLOAD,
UPDATER_INSTALL,
UPDATER_STATUS,
WINDOW_CLOSE,
WINDOW_FULLSCREEN_CHANGED,
WINDOW_IS_FULLSCREEN,
WINDOW_IS_MAXIMIZED,
WINDOW_MAXIMIZE,
WINDOW_MINIMIZE,
} from './constants/ipcChannels';
import {
CONFIG_ADD_CUSTOM_PROJECT_PATH,
CONFIG_ADD_IGNORE_REGEX,
CONFIG_ADD_IGNORE_REPOSITORY,
CONFIG_ADD_TRIGGER,
CONFIG_CLEAR_SNOOZE,
CONFIG_FIND_WSL_CLAUDE_ROOTS,
CONFIG_GET,
CONFIG_GET_CLAUDE_ROOT_INFO,
CONFIG_GET_TRIGGERS,
CONFIG_HIDE_SESSION,
CONFIG_HIDE_SESSIONS,
CONFIG_OPEN_IN_EDITOR,
CONFIG_PIN_SESSION,
CONFIG_REMOVE_CUSTOM_PROJECT_PATH,
CONFIG_REMOVE_IGNORE_REGEX,
CONFIG_REMOVE_IGNORE_REPOSITORY,
CONFIG_REMOVE_TRIGGER,
CONFIG_SELECT_CLAUDE_ROOT_FOLDER,
CONFIG_SELECT_FOLDERS,
CONFIG_SNOOZE,
CONFIG_TEST_TRIGGER,
CONFIG_UNHIDE_SESSION,
CONFIG_UNHIDE_SESSIONS,
CONFIG_UNPIN_SESSION,
CONFIG_UPDATE,
CONFIG_UPDATE_TRIGGER,
} from './constants/ipcChannels';
import type {
AddMemberRequest,
AgentChangeSet,
AppConfig,
ApplyReviewRequest,
ApplyReviewResult,
AttachmentFileData,
ChangeStats,
ClaudeRootFolderSelection,
ClaudeRootInfo,
CliInstallationStatus,
CliInstallerProgress,
ConflictCheckResult,
ContextInfo,
CreateTaskRequest,
ElectronAPI,
FileChangeWithContent,
GlobalTask,
HttpServerStatus,
HunkDecision,
IpcResult,
KanbanColumnId,
MemberFullStats,
MemberLogSummary,
NotificationTrigger,
RejectResult,
ReplaceMembersRequest,
SendMessageRequest,
SendMessageResult,
SessionsByIdsOptions,
SessionsPaginationOptions,
SnippetDiff,
SshConfigHostEntry,
SshConnectionConfig,
SshConnectionStatus,
SshLastConnection,
TaskChangeSetV2,
TaskComment,
TeamChangeEvent,
TeamConfig,
TeamCreateConfigRequest,
TeamCreateRequest,
TeamCreateResponse,
TeamData,
TeamLaunchRequest,
TeamLaunchResponse,
TeamMessageNotificationData,
TeamProvisioningPrepareResult,
TeamProvisioningProgress,
TeamSummary,
TeamTask,
TeamTaskStatus,
TeamUpdateConfigRequest,
TriggerTestResult,
UpdateKanbanPatch,
WslClaudeRootCandidate,
} from '@shared/types';
import type {
BinaryPreviewResult,
CreateDirResponse,
CreateFileResponse,
DeleteFileResponse,
EditorFileChangeEvent,
GitStatusResult,
MoveFileResponse,
QuickOpenFile,
ReadDirResult,
ReadFileResult,
SearchInFilesOptions,
SearchInFilesResult,
WriteFileResponse,
} from '@shared/types/editor';
import type { PtySpawnOptions } from '@shared/types/terminal';
// =============================================================================
// IPC Result Types and Helpers
// =============================================================================
interface IpcFileChangePayload {
type: 'add' | 'change' | 'unlink';
path: string;
projectId?: string;
sessionId?: string;
isSubagent: boolean;
}
/**
* Type-safe IPC invoker for operations that return IpcResult<T>.
* Throws an Error if the IPC call fails, otherwise returns the typed data.
*/
async function invokeIpcWithResult<T>(channel: string, ...args: unknown[]): Promise<T> {
const result = (await ipcRenderer.invoke(channel, ...args)) as IpcResult<T>;
if (!result.success) {
throw new Error(result.error ?? 'Unknown error');
}
return result.data as T;
}
// Keep latest zoom factor cached even before renderer UI subscribes.
let currentZoomFactor = 1;
ipcRenderer.on(
WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL,
(_event: Electron.IpcRendererEvent, zoomFactor: unknown) => {
if (typeof zoomFactor === 'number' && Number.isFinite(zoomFactor)) {
currentZoomFactor = zoomFactor;
}
}
);
// =============================================================================
// Electron API Implementation
// =============================================================================
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
const electronAPI: ElectronAPI = {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
getProjects: () => ipcRenderer.invoke('get-projects'),
getSessions: (projectId: string) => ipcRenderer.invoke('get-sessions', projectId),
getSessionsPaginated: (
projectId: string,
cursor: string | null,
limit?: number,
options?: SessionsPaginationOptions
) => ipcRenderer.invoke('get-sessions-paginated', projectId, cursor, limit, options),
searchSessions: (projectId: string, query: string, maxResults?: number) =>
ipcRenderer.invoke('search-sessions', projectId, query, maxResults),
searchAllProjects: (query: string, maxResults?: number) =>
ipcRenderer.invoke('search-all-projects', query, maxResults),
getSessionDetail: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-detail', projectId, sessionId),
getSessionMetrics: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-metrics', projectId, sessionId),
getWaterfallData: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-waterfall-data', projectId, sessionId),
getSubagentDetail: (projectId: string, sessionId: string, subagentId: string) =>
ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId),
getSessionGroups: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-groups', projectId, sessionId),
getSessionsByIds: (projectId: string, sessionIds: string[], options?: SessionsByIdsOptions) =>
ipcRenderer.invoke('get-sessions-by-ids', projectId, sessionIds, options),
// Repository grouping (worktree support)
getRepositoryGroups: () => ipcRenderer.invoke('get-repository-groups'),
getWorktreeSessions: (worktreeId: string) =>
ipcRenderer.invoke('get-worktree-sessions', worktreeId),
// Validation methods
validatePath: (relativePath: string, projectPath: string) =>
ipcRenderer.invoke('validate-path', relativePath, projectPath),
validateMentions: (mentions: { type: 'path'; value: string }[], projectPath: string) =>
ipcRenderer.invoke('validate-mentions', mentions, projectPath),
// CLAUDE.md reading methods
readClaudeMdFiles: (projectRoot: string) =>
ipcRenderer.invoke('read-claude-md-files', projectRoot),
readDirectoryClaudeMd: (dirPath: string) =>
ipcRenderer.invoke('read-directory-claude-md', dirPath),
readMentionedFile: (absolutePath: string, projectRoot: string, maxTokens?: number) =>
ipcRenderer.invoke('read-mentioned-file', absolutePath, projectRoot, maxTokens),
// Agent config reading
readAgentConfigs: (projectRoot: string) => ipcRenderer.invoke('read-agent-configs', projectRoot),
// Notifications API
notifications: {
get: (options?: { limit?: number; offset?: number }) =>
ipcRenderer.invoke('notifications:get', options),
markRead: (id: string) => ipcRenderer.invoke('notifications:markRead', id),
markAllRead: () => ipcRenderer.invoke('notifications:markAllRead'),
delete: (id: string) => ipcRenderer.invoke('notifications:delete', id),
clear: () => ipcRenderer.invoke('notifications:clear'),
getUnreadCount: () => ipcRenderer.invoke('notifications:getUnreadCount'),
onNew: (callback: (event: unknown, error: unknown) => void): (() => void) => {
ipcRenderer.on(
'notification:new',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
'notification:new',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
onUpdated: (
callback: (event: unknown, payload: { total: number; unreadCount: number }) => void
): (() => void) => {
ipcRenderer.on(
'notification:updated',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
'notification:updated',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
onClicked: (callback: (event: unknown, data: unknown) => void): (() => void) => {
ipcRenderer.on(
'notification:clicked',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
'notification:clicked',
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// Config API - uses typed helper to unwrap { success, data, error } responses
config: {
get: async (): Promise<AppConfig> => {
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
update: async (section: string, data: object): Promise<AppConfig> => {
return invokeIpcWithResult<AppConfig>(CONFIG_UPDATE, section, data);
},
addIgnoreRegex: async (pattern: string): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_ADD_IGNORE_REGEX, pattern);
// Re-fetch config after mutation
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
removeIgnoreRegex: async (pattern: string): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_REMOVE_IGNORE_REGEX, pattern);
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
addIgnoreRepository: async (repositoryId: string): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_ADD_IGNORE_REPOSITORY, repositoryId);
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
removeIgnoreRepository: async (repositoryId: string): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_REMOVE_IGNORE_REPOSITORY, repositoryId);
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
snooze: async (minutes: number): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_SNOOZE, minutes);
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
clearSnooze: async (): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_CLEAR_SNOOZE);
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
addTrigger: async (trigger: Omit<NotificationTrigger, 'isBuiltin'>): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_ADD_TRIGGER, trigger);
// Return updated config
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
updateTrigger: async (
triggerId: string,
updates: Partial<NotificationTrigger>
): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_UPDATE_TRIGGER, triggerId, updates);
// Return updated config
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
removeTrigger: async (triggerId: string): Promise<AppConfig> => {
await invokeIpcWithResult<void>(CONFIG_REMOVE_TRIGGER, triggerId);
// Return updated config
return invokeIpcWithResult<AppConfig>(CONFIG_GET);
},
getTriggers: async (): Promise<NotificationTrigger[]> => {
return invokeIpcWithResult<NotificationTrigger[]>(CONFIG_GET_TRIGGERS);
},
testTrigger: async (trigger: NotificationTrigger): Promise<TriggerTestResult> => {
return invokeIpcWithResult<TriggerTestResult>(CONFIG_TEST_TRIGGER, trigger);
},
selectFolders: async (): Promise<string[]> => {
return invokeIpcWithResult<string[]>(CONFIG_SELECT_FOLDERS);
},
selectClaudeRootFolder: async (): Promise<ClaudeRootFolderSelection | null> => {
return invokeIpcWithResult<ClaudeRootFolderSelection | null>(
CONFIG_SELECT_CLAUDE_ROOT_FOLDER
);
},
getClaudeRootInfo: async (): Promise<ClaudeRootInfo> => {
return invokeIpcWithResult<ClaudeRootInfo>(CONFIG_GET_CLAUDE_ROOT_INFO);
},
findWslClaudeRoots: async (): Promise<WslClaudeRootCandidate[]> => {
return invokeIpcWithResult<WslClaudeRootCandidate[]>(CONFIG_FIND_WSL_CLAUDE_ROOTS);
},
openInEditor: async (): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_OPEN_IN_EDITOR);
},
pinSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_PIN_SESSION, projectId, sessionId);
},
unpinSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNPIN_SESSION, projectId, sessionId);
},
hideSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_HIDE_SESSION, projectId, sessionId);
},
unhideSession: async (projectId: string, sessionId: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNHIDE_SESSION, projectId, sessionId);
},
hideSessions: async (projectId: string, sessionIds: string[]): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_HIDE_SESSIONS, projectId, sessionIds);
},
unhideSessions: async (projectId: string, sessionIds: string[]): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_UNHIDE_SESSIONS, projectId, sessionIds);
},
addCustomProjectPath: async (projectPath: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_ADD_CUSTOM_PROJECT_PATH, projectPath);
},
removeCustomProjectPath: async (projectPath: string): Promise<void> => {
return invokeIpcWithResult<void>(CONFIG_REMOVE_CUSTOM_PROJECT_PATH, projectPath);
},
},
// Deep link navigation
session: {
scrollToLine: (sessionId: string, lineNumber: number) =>
ipcRenderer.invoke('session:scrollToLine', sessionId, lineNumber),
},
// Zoom factor sync (used for traffic-light-safe layout)
getZoomFactor: async (): Promise<number> => currentZoomFactor,
onZoomFactorChanged: (callback: (zoomFactor: number) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, zoomFactor: unknown): void => {
if (typeof zoomFactor !== 'number' || !Number.isFinite(zoomFactor)) return;
currentZoomFactor = zoomFactor;
callback(zoomFactor);
};
ipcRenderer.on(WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL, listener);
return (): void => {
ipcRenderer.removeListener(WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL, listener);
};
},
// File change events (real-time updates)
onFileChange: (callback: (event: IpcFileChangePayload) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: IpcFileChangePayload): void =>
callback(data);
ipcRenderer.on('file-change', listener);
return (): void => {
ipcRenderer.removeListener('file-change', listener);
};
},
// Shell operations
openPath: (targetPath: string, projectRoot?: string, userSelectedFromDialog?: boolean) =>
ipcRenderer.invoke('shell:openPath', targetPath, projectRoot, userSelectedFromDialog),
showInFolder: (filePath: string) => ipcRenderer.invoke('shell:showInFolder', filePath),
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
// Window controls (when title bar is hidden, e.g. Windows / Linux)
windowControls: {
minimize: () => ipcRenderer.invoke(WINDOW_MINIMIZE),
maximize: () => ipcRenderer.invoke(WINDOW_MAXIMIZE),
close: () => ipcRenderer.invoke(WINDOW_CLOSE),
isMaximized: () => ipcRenderer.invoke(WINDOW_IS_MAXIMIZED) as Promise<boolean>,
isFullScreen: () => ipcRenderer.invoke(WINDOW_IS_FULLSCREEN) as Promise<boolean>,
relaunch: () => ipcRenderer.invoke(APP_RELAUNCH),
},
onFullScreenChange: (callback: (isFullScreen: boolean) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, isFullScreen: boolean): void =>
callback(isFullScreen);
ipcRenderer.on(WINDOW_FULLSCREEN_CHANGED, listener);
return (): void => {
ipcRenderer.removeListener(WINDOW_FULLSCREEN_CHANGED, listener);
};
},
onTodoChange: (callback: (event: IpcFileChangePayload) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: IpcFileChangePayload): void =>
callback(data);
ipcRenderer.on('todo-change', listener);
return (): void => {
ipcRenderer.removeListener('todo-change', listener);
};
},
// Updater API
updater: {
check: () => ipcRenderer.invoke(UPDATER_CHECK),
download: () => ipcRenderer.invoke(UPDATER_DOWNLOAD),
install: () => ipcRenderer.invoke(UPDATER_INSTALL),
onStatus: (callback: (event: unknown, status: unknown) => void): (() => void) => {
ipcRenderer.on(
UPDATER_STATUS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
UPDATER_STATUS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// SSH API
ssh: {
connect: async (config: SshConnectionConfig): Promise<SshConnectionStatus> => {
return invokeIpcWithResult<SshConnectionStatus>(SSH_CONNECT, config);
},
disconnect: async (): Promise<SshConnectionStatus> => {
return invokeIpcWithResult<SshConnectionStatus>(SSH_DISCONNECT);
},
getState: async (): Promise<SshConnectionStatus> => {
return invokeIpcWithResult<SshConnectionStatus>(SSH_GET_STATE);
},
test: async (config: SshConnectionConfig): Promise<{ success: boolean; error?: string }> => {
return invokeIpcWithResult<{ success: boolean; error?: string }>(SSH_TEST, config);
},
getConfigHosts: async (): Promise<SshConfigHostEntry[]> => {
return invokeIpcWithResult<SshConfigHostEntry[]>(SSH_GET_CONFIG_HOSTS);
},
resolveHost: async (alias: string): Promise<SshConfigHostEntry | null> => {
return invokeIpcWithResult<SshConfigHostEntry | null>(SSH_RESOLVE_HOST, alias);
},
saveLastConnection: async (config: SshLastConnection): Promise<void> => {
return invokeIpcWithResult<void>(SSH_SAVE_LAST_CONNECTION, config);
},
getLastConnection: async (): Promise<SshLastConnection | null> => {
return invokeIpcWithResult<SshLastConnection | null>(SSH_GET_LAST_CONNECTION);
},
onStatus: (callback: (event: unknown, status: SshConnectionStatus) => void): (() => void) => {
ipcRenderer.on(
SSH_STATUS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
SSH_STATUS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// Context API
context: {
list: async (): Promise<ContextInfo[]> => {
return invokeIpcWithResult<ContextInfo[]>(CONTEXT_LIST);
},
getActive: async (): Promise<string> => {
return invokeIpcWithResult<string>(CONTEXT_GET_ACTIVE);
},
switch: async (contextId: string): Promise<{ contextId: string }> => {
return invokeIpcWithResult<{ contextId: string }>(CONTEXT_SWITCH, contextId);
},
onChanged: (callback: (event: unknown, data: ContextInfo) => void): (() => void) => {
ipcRenderer.on(
CONTEXT_CHANGED,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
CONTEXT_CHANGED,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// HTTP Server API
httpServer: {
start: async (): Promise<HttpServerStatus> => {
return invokeIpcWithResult<HttpServerStatus>(HTTP_SERVER_START);
},
stop: async (): Promise<HttpServerStatus> => {
return invokeIpcWithResult<HttpServerStatus>(HTTP_SERVER_STOP);
},
getStatus: async (): Promise<HttpServerStatus> => {
return invokeIpcWithResult<HttpServerStatus>(HTTP_SERVER_GET_STATUS);
},
},
teams: {
list: async () => {
return invokeIpcWithResult<TeamSummary[]>(TEAM_LIST);
},
getData: async (teamName: string) => {
return invokeIpcWithResult<TeamData>(TEAM_GET_DATA, teamName);
},
deleteTeam: async (teamName: string) => {
return invokeIpcWithResult<void>(TEAM_DELETE_TEAM, teamName);
},
restoreTeam: async (teamName: string) => {
return invokeIpcWithResult<void>(TEAM_RESTORE, teamName);
},
permanentlyDeleteTeam: async (teamName: string) => {
return invokeIpcWithResult<void>(TEAM_PERMANENTLY_DELETE, teamName);
},
prepareProvisioning: async (cwd?: string) => {
return invokeIpcWithResult<TeamProvisioningPrepareResult>(TEAM_PREPARE_PROVISIONING, cwd);
},
createTeam: async (request: TeamCreateRequest) => {
return invokeIpcWithResult<TeamCreateResponse>(TEAM_CREATE, request);
},
launchTeam: async (request: TeamLaunchRequest) => {
return invokeIpcWithResult<TeamLaunchResponse>(TEAM_LAUNCH, request);
},
getProvisioningStatus: async (runId: string) => {
return invokeIpcWithResult<TeamProvisioningProgress>(TEAM_PROVISIONING_STATUS, runId);
},
cancelProvisioning: async (runId: string) => {
return invokeIpcWithResult<void>(TEAM_CANCEL_PROVISIONING, runId);
},
sendMessage: async (teamName: string, request: SendMessageRequest) => {
return invokeIpcWithResult<SendMessageResult>(TEAM_SEND_MESSAGE, teamName, request);
},
createTask: async (teamName: string, request: CreateTaskRequest) => {
return invokeIpcWithResult<TeamTask>(TEAM_CREATE_TASK, teamName, request);
},
requestReview: async (teamName: string, taskId: string) => {
return invokeIpcWithResult<void>(TEAM_REQUEST_REVIEW, teamName, taskId);
},
updateKanban: async (teamName: string, taskId: string, patch: UpdateKanbanPatch) => {
return invokeIpcWithResult<void>(TEAM_UPDATE_KANBAN, teamName, taskId, patch);
},
updateKanbanColumnOrder: async (
teamName: string,
columnId: KanbanColumnId,
orderedTaskIds: string[]
) => {
return invokeIpcWithResult<void>(
TEAM_UPDATE_KANBAN_COLUMN_ORDER,
teamName,
columnId,
orderedTaskIds
);
},
updateTaskStatus: async (teamName: string, taskId: string, status: TeamTaskStatus) => {
return invokeIpcWithResult<void>(TEAM_UPDATE_TASK_STATUS, teamName, taskId, status);
},
updateTaskOwner: async (teamName: string, taskId: string, owner: string | null) => {
return invokeIpcWithResult<void>(TEAM_UPDATE_TASK_OWNER, teamName, taskId, owner);
},
updateTaskFields: async (
teamName: string,
taskId: string,
fields: { subject?: string; description?: string }
) => {
return invokeIpcWithResult<void>(TEAM_UPDATE_TASK_FIELDS, teamName, taskId, fields);
},
startTask: async (teamName: string, taskId: string) => {
return invokeIpcWithResult<{ notifiedOwner: boolean }>(TEAM_START_TASK, teamName, taskId);
},
processSend: async (teamName: string, message: string) => {
return invokeIpcWithResult<void>(TEAM_PROCESS_SEND, teamName, message);
},
processAlive: async (teamName: string) => {
return invokeIpcWithResult<boolean>(TEAM_PROCESS_ALIVE, teamName);
},
aliveList: async () => {
return invokeIpcWithResult<string[]>(TEAM_ALIVE_LIST);
},
stop: async (teamName: string) => {
return invokeIpcWithResult<void>(TEAM_STOP, teamName);
},
createConfig: async (request: TeamCreateConfigRequest) => {
return invokeIpcWithResult<void>(TEAM_CREATE_CONFIG, request);
},
getMemberLogs: async (teamName: string, memberName: string) => {
return invokeIpcWithResult<MemberLogSummary[]>(TEAM_GET_MEMBER_LOGS, teamName, memberName);
},
getLogsForTask: async (
teamName: string,
taskId: string,
options?: {
owner?: string;
status?: string;
intervals?: { startedAt: string; completedAt?: string }[];
since?: string;
}
) => {
return invokeIpcWithResult<MemberLogSummary[]>(
TEAM_GET_LOGS_FOR_TASK,
teamName,
taskId,
options
);
},
getMemberStats: async (teamName: string, memberName: string) => {
return invokeIpcWithResult<MemberFullStats>(TEAM_GET_MEMBER_STATS, teamName, memberName);
},
getAllTasks: async () => {
return invokeIpcWithResult<GlobalTask[]>(TEAM_GET_ALL_TASKS);
},
updateConfig: async (teamName: string, updates: TeamUpdateConfigRequest) => {
return invokeIpcWithResult<TeamConfig>(TEAM_UPDATE_CONFIG, teamName, updates);
},
addTaskComment: async (teamName: string, taskId: string, text: string) => {
return invokeIpcWithResult<TaskComment>(TEAM_ADD_TASK_COMMENT, teamName, taskId, text);
},
addMember: async (teamName: string, request: AddMemberRequest) => {
return invokeIpcWithResult<void>(TEAM_ADD_MEMBER, teamName, request);
},
replaceMembers: async (teamName: string, request: ReplaceMembersRequest) => {
return invokeIpcWithResult<void>(TEAM_REPLACE_MEMBERS, teamName, request);
},
removeMember: async (teamName: string, memberName: string) => {
return invokeIpcWithResult<void>(TEAM_REMOVE_MEMBER, teamName, memberName);
},
updateMemberRole: async (teamName: string, memberName: string, role: string | undefined) => {
return invokeIpcWithResult<void>(TEAM_UPDATE_MEMBER_ROLE, teamName, memberName, role);
},
getProjectBranch: async (projectPath: string) => {
return invokeIpcWithResult<string | null>(TEAM_GET_PROJECT_BRANCH, projectPath);
},
getAttachments: async (teamName: string, messageId: string) => {
return invokeIpcWithResult<AttachmentFileData[]>(TEAM_GET_ATTACHMENTS, teamName, messageId);
},
killProcess: async (teamName: string, pid: number) => {
return invokeIpcWithResult<void>(TEAM_KILL_PROCESS, teamName, pid);
},
getLeadActivity: async (teamName: string) => {
const result = await invokeIpcWithResult<string>(TEAM_LEAD_ACTIVITY, teamName);
return result as 'active' | 'idle' | 'offline';
},
softDeleteTask: async (teamName: string, taskId: string) => {
return invokeIpcWithResult<void>(TEAM_SOFT_DELETE_TASK, teamName, taskId);
},
restoreTask: async (teamName: string, taskId: string) => {
return invokeIpcWithResult<void>(TEAM_RESTORE_TASK, teamName, taskId);
},
getDeletedTasks: async (teamName: string) => {
return invokeIpcWithResult<TeamTask[]>(TEAM_GET_DELETED_TASKS, teamName);
},
setTaskClarification: async (
teamName: string,
taskId: string,
value: 'lead' | 'user' | null
) => {
return invokeIpcWithResult<void>(TEAM_SET_TASK_CLARIFICATION, teamName, taskId, value);
},
showMessageNotification: async (data: TeamMessageNotificationData) => {
return invokeIpcWithResult<void>(TEAM_SHOW_MESSAGE_NOTIFICATION, data);
},
addTaskRelationship: async (
teamName: string,
taskId: string,
targetId: string,
type: 'blockedBy' | 'blocks' | 'related'
) => {
return invokeIpcWithResult<void>(
TEAM_ADD_TASK_RELATIONSHIP,
teamName,
taskId,
targetId,
type
);
},
removeTaskRelationship: async (
teamName: string,
taskId: string,
targetId: string,
type: 'blockedBy' | 'blocks' | 'related'
) => {
return invokeIpcWithResult<void>(
TEAM_REMOVE_TASK_RELATIONSHIP,
teamName,
taskId,
targetId,
type
);
},
onTeamChange: (callback: (event: unknown, data: TeamChangeEvent) => void): (() => void) => {
ipcRenderer.on(
TEAM_CHANGE,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
TEAM_CHANGE,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
onProvisioningProgress: (
callback: (event: unknown, data: TeamProvisioningProgress) => void
): (() => void) => {
ipcRenderer.on(
TEAM_PROVISIONING_PROGRESS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
TEAM_PROVISIONING_PROGRESS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// ===== Review API =====
review: {
getAgentChanges: async (teamName: string, memberName: string) => {
return invokeIpcWithResult<AgentChangeSet>(REVIEW_GET_AGENT_CHANGES, teamName, memberName);
},
getTaskChanges: async (teamName: string, taskId: string) => {
return invokeIpcWithResult<TaskChangeSetV2>(REVIEW_GET_TASK_CHANGES, teamName, taskId);
},
getChangeStats: async (teamName: string, memberName: string) => {
return invokeIpcWithResult<ChangeStats>(REVIEW_GET_CHANGE_STATS, teamName, memberName);
},
getFileContent: async (
teamName: string,
memberName: string | undefined,
filePath: string,
snippets: SnippetDiff[] = []
) => {
return invokeIpcWithResult<FileChangeWithContent>(
REVIEW_GET_FILE_CONTENT,
teamName,
memberName ?? '',
filePath,
snippets
);
},
applyDecisions: async (request: ApplyReviewRequest) => {
return invokeIpcWithResult<ApplyReviewResult>(REVIEW_APPLY_DECISIONS, request);
},
// Phase 2
checkConflict: async (filePath: string, expectedModified: string) => {
return invokeIpcWithResult<ConflictCheckResult>(
REVIEW_CHECK_CONFLICT,
filePath,
expectedModified
);
},
rejectHunks: async (
filePath: string,
original: string,
modified: string,
hunkIndices: number[],
snippets: SnippetDiff[]
) => {
return invokeIpcWithResult<RejectResult>(
REVIEW_REJECT_HUNKS,
filePath,
original,
modified,
hunkIndices,
snippets
);
},
rejectFile: async (filePath: string, original: string, modified: string) => {
return invokeIpcWithResult<RejectResult>(REVIEW_REJECT_FILE, filePath, original, modified);
},
previewReject: async (
filePath: string,
original: string,
modified: string,
hunkIndices: number[],
snippets: SnippetDiff[]
) => {
return invokeIpcWithResult<{ preview: string; hasConflicts: boolean }>(
REVIEW_PREVIEW_REJECT,
filePath,
original,
modified,
hunkIndices,
snippets
);
},
// Editable diff
saveEditedFile: async (filePath: string, content: string, projectPath?: string) => {
return invokeIpcWithResult<{ success: boolean }>(
REVIEW_SAVE_EDITED_FILE,
filePath,
content,
projectPath
);
},
// Decision persistence
loadDecisions: async (teamName: string, scopeKey: string) => {
return invokeIpcWithResult<{
hunkDecisions: Record<string, HunkDecision>;
fileDecisions: Record<string, HunkDecision>;
hunkContextHashesByFile?: Record<string, Record<number, string>>;
} | null>(REVIEW_LOAD_DECISIONS, teamName, scopeKey);
},
saveDecisions: async (
teamName: string,
scopeKey: string,
hunkDecisions: Record<string, HunkDecision>,
fileDecisions: Record<string, HunkDecision>,
hunkContextHashesByFile?: Record<string, Record<number, string>>
) => {
return invokeIpcWithResult<void>(
REVIEW_SAVE_DECISIONS,
teamName,
scopeKey,
hunkDecisions,
fileDecisions,
hunkContextHashesByFile ?? null
);
},
clearDecisions: async (teamName: string, scopeKey: string) => {
return invokeIpcWithResult<void>(REVIEW_CLEAR_DECISIONS, teamName, scopeKey);
},
onCmdN: (callback: () => void): (() => void) => {
const handler = (): void => callback();
ipcRenderer.on('review:cmdN', handler);
return (): void => {
ipcRenderer.removeListener('review:cmdN', handler);
};
},
// Phase 4
getGitFileLog: async (projectPath: string, filePath: string) => {
return invokeIpcWithResult<{ hash: string; timestamp: string; message: string }[]>(
REVIEW_GET_GIT_FILE_LOG,
projectPath,
filePath
);
},
},
// ===== CLI Installer API =====
cliInstaller: {
getStatus: async (): Promise<CliInstallationStatus> => {
return invokeIpcWithResult<CliInstallationStatus>(CLI_INSTALLER_GET_STATUS);
},
install: async (): Promise<void> => {
return invokeIpcWithResult<void>(CLI_INSTALLER_INSTALL);
},
onProgress: (callback: (event: unknown, data: CliInstallerProgress) => void): (() => void) => {
ipcRenderer.on(
CLI_INSTALLER_PROGRESS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
CLI_INSTALLER_PROGRESS,
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// ===== Terminal API =====
terminal: {
spawn: (options?: PtySpawnOptions) => invokeIpcWithResult<string>(TERMINAL_SPAWN, options),
write: (ptyId: string, data: string) => ipcRenderer.send(TERMINAL_WRITE, ptyId, data),
resize: (ptyId: string, cols: number, rows: number) =>
ipcRenderer.send(TERMINAL_RESIZE, ptyId, cols, rows),
kill: (ptyId: string) => ipcRenderer.send(TERMINAL_KILL, ptyId),
onData: (cb: (event: unknown, ptyId: string, data: string) => void): (() => void) => {
ipcRenderer.on(
TERMINAL_DATA,
cb as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
TERMINAL_DATA,
cb as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
onExit: (cb: (event: unknown, ptyId: string, exitCode: number) => void): (() => void) => {
ipcRenderer.on(
TERMINAL_EXIT,
cb as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
return (): void => {
ipcRenderer.removeListener(
TERMINAL_EXIT,
cb as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
);
};
},
},
// ===== Project API (editor-independent) =====
project: {
listFiles: (projectPath: string) =>
invokeIpcWithResult<QuickOpenFile[]>(PROJECT_LIST_FILES, projectPath),
},
// ===== Editor API =====
editor: {
open: (projectPath: string) => invokeIpcWithResult<void>(EDITOR_OPEN, projectPath),
close: () => invokeIpcWithResult<void>(EDITOR_CLOSE),
readDir: (dirPath: string, maxEntries?: number) =>
invokeIpcWithResult<ReadDirResult>(EDITOR_READ_DIR, dirPath, maxEntries),
readFile: (filePath: string) => invokeIpcWithResult<ReadFileResult>(EDITOR_READ_FILE, filePath),
writeFile: (filePath: string, content: string, baselineMtimeMs?: number) =>
invokeIpcWithResult<WriteFileResponse>(EDITOR_WRITE_FILE, filePath, content, baselineMtimeMs),
createFile: (parentDir: string, fileName: string) =>
invokeIpcWithResult<CreateFileResponse>(EDITOR_CREATE_FILE, parentDir, fileName),
createDir: (parentDir: string, dirName: string) =>
invokeIpcWithResult<CreateDirResponse>(EDITOR_CREATE_DIR, parentDir, dirName),
deleteFile: (filePath: string) =>
invokeIpcWithResult<DeleteFileResponse>(EDITOR_DELETE_FILE, filePath),
moveFile: (sourcePath: string, destDir: string) =>
invokeIpcWithResult<MoveFileResponse>(EDITOR_MOVE_FILE, sourcePath, destDir),
renameFile: (sourcePath: string, newName: string) =>
invokeIpcWithResult<MoveFileResponse>(EDITOR_RENAME_FILE, sourcePath, newName),
searchInFiles: (options: SearchInFilesOptions) =>
invokeIpcWithResult<SearchInFilesResult>(EDITOR_SEARCH_IN_FILES, options),
listFiles: () => invokeIpcWithResult<QuickOpenFile[]>(EDITOR_LIST_FILES),
readBinaryPreview: (filePath: string) =>
invokeIpcWithResult<BinaryPreviewResult>(EDITOR_READ_BINARY_PREVIEW, filePath),
gitStatus: () => invokeIpcWithResult<GitStatusResult>(EDITOR_GIT_STATUS),
watchDir: (enable: boolean) => invokeIpcWithResult<void>(EDITOR_WATCH_DIR, enable),
setWatchedFiles: (filePaths: string[]) =>
invokeIpcWithResult<void>(EDITOR_SET_WATCHED_FILES, filePaths),
setWatchedDirs: (dirPaths: string[]) =>
invokeIpcWithResult<void>(EDITOR_SET_WATCHED_DIRS, dirPaths),
onEditorChange: (callback: (event: EditorFileChangeEvent) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: EditorFileChangeEvent): void =>
callback(data);
ipcRenderer.on(EDITOR_CHANGE, listener);
return (): void => {
ipcRenderer.removeListener(EDITOR_CHANGE, listener);
};
},
},
};
// Use contextBridge to securely expose the API to the renderer process
contextBridge.exposeInMainWorld('electronAPI', electronAPI);