Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
OPENAI_API_KEY="xxx"
ANTHROPIC_API_KEY="xxx"

APPLE_CODESIGN_IDENTITY="Developer ID Application: PostHog Inc. (xxx)"
APPLE_ID="[email protected]"
APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxx-xxx-xxx"
Expand Down
5 changes: 2 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ Import directly from source files instead.
## Environment Variables

- Copy `.env.example` to `.env`
- `ANTHROPIC_API_KEY` - Required for agent
- `OPENAI_API_KEY` - Optional
- `VITE_POSTHOG_*` - PostHog tracking config

TODO: Update me

## Testing

Expand Down
4 changes: 4 additions & 0 deletions apps/array/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
"@tiptap/react": "^3.11.0",
"@tiptap/starter-kit": "^3.11.0",
"@tiptap/suggestion": "^3.11.0",
"@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/addon-web-links": "^0.11.0",
Expand Down Expand Up @@ -142,6 +145,7 @@
"react-resizable-panels": "^3.0.6",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"trpc-electron": "^0.1.2",
"uuid": "^9.0.1",
"vscode-icons-js": "^11.6.1",
"zod": "^4.1.12",
Expand Down
14 changes: 7 additions & 7 deletions apps/array/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import {
type MenuItemConstructorOptions,
shell,
} from "electron";
import { createIPCHandler } from "trpc-electron/main";
import "./lib/logger";
import { ANALYTICS_EVENTS } from "../types/analytics.js";
import { dockBadgeService } from "./services/dockBadge.js";
import {
cleanupAgentSessions,
registerAgentIpc,
} from "./services/session-manager.js";
import { setMainWindowGetter } from "./trpc/context.js";
import { trpcRouter } from "./trpc/index.js";

// Legacy type kept for backwards compatibility with taskControllers map
type TaskController = unknown;
Expand All @@ -37,14 +40,11 @@ import {
registerExternalAppsIpc,
} from "./services/externalApps.js";
import { registerOAuthHandlers } from "./services/oauth.js";
import { registerOsIpc } from "./services/os.js";
import { registerPosthogIpc } from "./services/posthog.js";
import {
initializePostHog,
shutdownPostHog,
trackAppEvent,
} from "./services/posthog-analytics.js";
import { registerSettingsIpc } from "./services/settings.js";
import { registerShellIpc } from "./services/shell.js";
import { registerAutoUpdater } from "./services/updates.js";
import { registerWorkspaceIpc } from "./services/workspace/index.js";
Expand Down Expand Up @@ -115,6 +115,9 @@ function createWindow(): void {
mainWindow?.show();
});

setMainWindowGetter(() => mainWindow);
createIPCHandler({ router: trpcRouter, windows: [mainWindow] });

setupExternalLinkHandlers(mainWindow);

// Set up menu for keyboard shortcuts
Expand Down Expand Up @@ -293,10 +296,8 @@ registerAutoUpdater(() => mainWindow);
ipcMain.handle("app:get-version", () => app.getVersion());

// Register IPC handlers via services
registerPosthogIpc();
registerOAuthHandlers();
registerOsIpc(() => mainWindow);
registerGitIpc(() => mainWindow);
registerGitIpc();
registerAgentIpc(taskControllers, () => mainWindow);
registerFsIpc();
registerFileWatcherIpc(() => mainWindow);
Expand All @@ -305,4 +306,3 @@ registerWorktreeIpc();
registerShellIpc();
registerExternalAppsIpc();
registerWorkspaceIpc(() => mainWindow);
registerSettingsIpc();
152 changes: 10 additions & 142 deletions apps/array/src/main/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ContentBlock } from "@agentclientprotocol/sdk";
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron";
import { exposeElectronTRPC } from "trpc-electron/main";
import type {
CreateWorkspaceOptions,
RegisteredFolder,
Expand All @@ -19,6 +20,12 @@ import type {
} from "./services/contextMenu.types.js";
import "electron-log/preload";

process.once("loaded", () => {
exposeElectronTRPC();
});

/// -- Legacy IPC handlers -- ///

type IpcEventListener<T> = (data: T) => void;

function createIpcListener<T>(
Expand All @@ -38,16 +45,6 @@ function createVoidIpcListener(
return () => ipcRenderer.removeListener(channel, listener);
}

interface MessageBoxOptions {
type?: "none" | "info" | "error" | "question" | "warning";
title?: string;
message?: string;
detail?: string;
buttons?: string[];
defaultId?: number;
cancelId?: number;
}

interface AgentStartParams {
taskId: string;
taskRunId: string;
Expand All @@ -64,20 +61,6 @@ interface AgentStartParams {
}

contextBridge.exposeInMainWorld("electronAPI", {
storeApiKey: (apiKey: string): Promise<string> =>
ipcRenderer.invoke("store-api-key", apiKey),
retrieveApiKey: (encryptedKey: string): Promise<string | null> =>
ipcRenderer.invoke("retrieve-api-key", encryptedKey),
fetchS3Logs: (logUrl: string): Promise<string | null> =>
ipcRenderer.invoke("fetch-s3-logs", logUrl),
rendererStore: {
getItem: (key: string): Promise<string | null> =>
ipcRenderer.invoke("renderer-store:get", key),
setItem: (key: string, value: string): Promise<void> =>
ipcRenderer.invoke("renderer-store:set", key, value),
removeItem: (key: string): Promise<void> =>
ipcRenderer.invoke("renderer-store:remove", key),
},
// OAuth API
oauthStartFlow: (
region: CloudRegion,
Expand All @@ -90,16 +73,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
ipcRenderer.invoke("oauth:refresh-token", refreshToken, region),
oauthCancelFlow: (): Promise<{ success: boolean; error?: string }> =>
ipcRenderer.invoke("oauth:cancel-flow"),
selectDirectory: (): Promise<string | null> =>
ipcRenderer.invoke("select-directory"),
searchDirectories: (query: string, searchRoot?: string): Promise<string[]> =>
ipcRenderer.invoke("search-directories", query, searchRoot),
findReposDirectory: (): Promise<string | null> =>
ipcRenderer.invoke("find-repos-directory"),
// Repo API
validateRepo: (directoryPath: string): Promise<boolean> =>
ipcRenderer.invoke("validate-repo", directoryPath),
checkWriteAccess: (directoryPath: string): Promise<boolean> =>
ipcRenderer.invoke("check-write-access", directoryPath),
detectRepo: (
directoryPath: string,
): Promise<{
Expand All @@ -108,25 +84,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
branch?: string;
remote?: string;
} | null> => ipcRenderer.invoke("detect-repo", directoryPath),
validateRepositoryMatch: (
path: string,
organization: string,
repository: string,
): Promise<{
valid: boolean;
detected?: { organization: string; repository: string } | null;
error?: string;
}> =>
ipcRenderer.invoke(
"validate-repository-match",
path,
organization,
repository,
),
checkSSHAccess: (): Promise<{
available: boolean;
error?: string;
}> => ipcRenderer.invoke("check-ssh-access"),
cloneRepository: (
repoUrl: string,
targetPath: string,
Expand All @@ -140,18 +97,13 @@ contextBridge.exposeInMainWorld("electronAPI", {
message: string;
}) => void,
): (() => void) => createIpcListener(`clone-progress:${cloneId}`, listener),
showMessageBox: (options: MessageBoxOptions): Promise<{ response: number }> =>
ipcRenderer.invoke("show-message-box", options),
openExternal: (url: string): Promise<void> =>
ipcRenderer.invoke("open-external", url),
listRepoFiles: (
repoPath: string,
query?: string,
limit?: number,
): Promise<Array<{ path: string; name: string }>> =>
ipcRenderer.invoke("list-repo-files", repoPath, query, limit),
clearRepoFileCache: (repoPath: string): Promise<void> =>
ipcRenderer.invoke("clear-repo-file-cache", repoPath),
// Agent API
agentStart: async (
params: AgentStartParams,
): Promise<{ sessionId: string; channel: string }> =>
Expand All @@ -165,18 +117,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
ipcRenderer.invoke("agent-cancel", sessionId),
agentCancelPrompt: async (sessionId: string): Promise<boolean> =>
ipcRenderer.invoke("agent-cancel-prompt", sessionId),
agentListSessions: async (
taskId?: string,
): Promise<
Array<{
sessionId: string;
acpSessionId: string;
channel: string;
taskId: string;
}>
> => ipcRenderer.invoke("agent-list-sessions", taskId),
agentLoadSession: async (sessionId: string, cwd: string): Promise<boolean> =>
ipcRenderer.invoke("agent-load-session", sessionId, cwd),
agentReconnect: async (params: {
taskId: string;
taskRunId: string;
Expand All @@ -200,68 +140,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
listener: (payload: unknown) => void,
): (() => void) => createIpcListener(channel, listener),
// Plan mode operations
agentStartPlanMode: async (params: {
taskId: string;
taskTitle: string;
taskDescription: string;
repoPath: string;
apiKey: string;
apiHost: string;
projectId: number;
}): Promise<{ taskId: string; channel: string }> =>
ipcRenderer.invoke("agent-start-plan-mode", params),
agentGeneratePlan: async (params: {
taskId: string;
taskTitle: string;
taskDescription: string;
repoPath: string;
questionAnswers: unknown[];
apiKey: string;
apiHost: string;
projectId: number;
}): Promise<{ taskId: string; channel: string }> =>
ipcRenderer.invoke("agent-generate-plan", params),
readPlanFile: (repoPath: string, taskId: string): Promise<string | null> =>
ipcRenderer.invoke("read-plan-file", repoPath, taskId),
writePlanFile: (
repoPath: string,
taskId: string,
content: string,
): Promise<void> =>
ipcRenderer.invoke("write-plan-file", repoPath, taskId, content),
ensurePosthogFolder: (repoPath: string, taskId: string): Promise<string> =>
ipcRenderer.invoke("ensure-posthog-folder", repoPath, taskId),
listTaskArtifacts: (repoPath: string, taskId: string): Promise<unknown[]> =>
ipcRenderer.invoke("list-task-artifacts", repoPath, taskId),
readTaskArtifact: (
repoPath: string,
taskId: string,
fileName: string,
): Promise<string | null> =>
ipcRenderer.invoke("read-task-artifact", repoPath, taskId, fileName),
appendToArtifact: (
repoPath: string,
taskId: string,
fileName: string,
content: string,
): Promise<void> =>
ipcRenderer.invoke(
"append-to-artifact",
repoPath,
taskId,
fileName,
content,
),
saveQuestionAnswers: (
repoPath: string,
taskId: string,
answers: Array<{
questionId: string;
selectedOption: string;
customInput?: string;
}>,
): Promise<void> =>
ipcRenderer.invoke("save-question-answers", repoPath, taskId, answers),
readRepoFile: (repoPath: string, filePath: string): Promise<string | null> =>
ipcRenderer.invoke("read-repo-file", repoPath, filePath),
writeRepoFile: (
Expand Down Expand Up @@ -467,6 +345,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
): Promise<string | null> =>
ipcRenderer.invoke("worktree-get-main-repo", mainRepoPath, worktreePath),
},
// External Apps API
externalApps: {
getDetectedApps: (): Promise<
Array<{
Expand Down Expand Up @@ -533,17 +412,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
}>,
): (() => void) => createIpcListener("workspace:warning", listener),
},
// Settings API
settings: {
getWorktreeLocation: (): Promise<string> =>
ipcRenderer.invoke("settings:get-worktree-location"),
setWorktreeLocation: (location: string): Promise<void> =>
ipcRenderer.invoke("settings:set-worktree-location", location),
getTerminalLayout: (): Promise<"split" | "tabbed"> =>
ipcRenderer.invoke("settings:get-terminal-layout"),
setTerminalLayout: (mode: "split" | "tabbed"): Promise<void> =>
ipcRenderer.invoke("settings:set-terminal-layout", mode),
},
// Dock Badge API
dockBadge: {
show: (): Promise<void> => ipcRenderer.invoke("dock-badge:show"),
Expand Down
2 changes: 1 addition & 1 deletion apps/array/src/main/services/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import type { RegisteredFolder } from "../../shared/types";
import { generateId } from "../../shared/utils/id";
import { createIpcHandler } from "../lib/ipcHandler";
import { logger } from "../lib/logger";
import { clearAllStoreData, foldersStore } from "../utils/store";
import { isGitRepository } from "./git";
import { getWorktreeLocation } from "./settingsStore";
import { clearAllStoreData, foldersStore } from "./store";
import { deleteWorktreeIfExists } from "./worktreeUtils";

const execAsync = promisify(exec);
Expand Down
Loading