aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-28 07:27:49 -0800
committerClawd <ai@clawd.bot>2026-02-28 07:27:49 -0800
commit66f66d1c17213f55aa56d69c0cccc309b16f3362 (patch)
tree5a6464c7f36e7c731f6d07995856df00bce257a4 /src
parent332e5cec2992fefb302251962a3ceca38437a110 (diff)
Phase 3: IPC layer
- Implement src/main/preload.ts with typed API bridge - Projects, sessions, messages CRUD - Chat send/interrupt - Workflow review/advance/permissions - Artifact read/write - Directory picker dialog - Claude message event subscription - Implement src/main/ipc/handlers.ts - All IPC handlers with proper error handling - Message forwarding to renderer - Assistant message storage - Update src/main/index.ts - Initialize database on startup - Register IPC handlers - Clean database close on exit
Diffstat (limited to 'src')
-rw-r--r--src/main/index.ts90
-rw-r--r--src/main/ipc/handlers.ts117
-rw-r--r--src/main/preload.ts115
3 files changed, 275 insertions, 47 deletions
diff --git a/src/main/index.ts b/src/main/index.ts
index b164f15..f0b23f7 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,63 +1,61 @@
1import Database from 'better-sqlite3' 1import { app, BrowserWindow } from "electron";
2import { app, BrowserWindow } from 'electron' 2import path from "node:path";
3import fs from 'node:fs' 3import { getDb, closeDb } from "./db";
4import path from 'node:path' 4import { registerIpcHandlers } from "./ipc/handlers";
5 5
6const isDev = !app.isPackaged // reliable dev/prod switch. [oai_citation:2‡Electron](https://electronjs.org/docs/latest/api/app?utm_source=chatgpt.com) 6const isDev = !app.isPackaged;
7let mainWindow: BrowserWindow | null = null;
7 8
8function createWindow() { 9function createWindow() {
9 const win = new BrowserWindow({ 10 mainWindow = new BrowserWindow({
10 width: 800, 11 width: 1400,
11 height: 600, 12 height: 900,
13 minWidth: 1000,
14 minHeight: 600,
12 show: false, 15 show: false,
16 titleBarStyle: "hiddenInset",
13 webPreferences: { 17 webPreferences: {
14 contextIsolation: true, 18 contextIsolation: true,
15 nodeIntegration: false, 19 nodeIntegration: false,
16 preload: path.join(__dirname, 'preload.js'), 20 preload: path.join(__dirname, "preload.js"),
17 }, 21 },
18 }) 22 });
23
24 registerIpcHandlers(mainWindow);
19 25
20 if (isDev) { 26 if (isDev) {
21 const url = process.env.VITE_DEV_SERVER_URL ?? 'http://localhost:5173' 27 const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173";
22 win.loadURL(url).finally(() => { 28 mainWindow.loadURL(url).finally(() => {
23 win.show() 29 mainWindow!.show();
24 win.webContents.openDevTools({ mode: 'detach' }) 30 mainWindow!.webContents.openDevTools({ mode: "detach" });
25 }) 31 });
26 } else { 32 } else {
27 const indexHtml = path.join( 33 const indexHtml = path.join(
28 app.getAppPath(), 34 app.getAppPath(),
29 'renderer', 35 "renderer",
30 'dist', 36 "dist",
31 'index.html' 37 "index.html"
32 ) 38 );
33 win.loadFile(indexHtml).finally(() => win.show()) 39 mainWindow.loadFile(indexHtml).finally(() => mainWindow!.show());
34 } 40 }
35} 41}
36 42
37function initDb() {
38 const dbDir = app.getPath('userData')
39 if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true })
40 const dbPath = path.join(dbDir, 'app.db')
41
42 const db = new Database(dbPath)
43 db.pragma('journal_mode = WAL')
44 db.prepare('CREATE TABLE IF NOT EXISTS messages (text TEXT)').run()
45 db.prepare('INSERT INTO messages (text) VALUES (?)').run(
46 'hello from better-sqlite3'
47 )
48 const row = db.prepare('SELECT text FROM messages LIMIT 1').get()
49 console.log('Selected row:', row)
50 db.close()
51}
52
53app.whenReady().then(() => { 43app.whenReady().then(() => {
54 initDb() 44 // Initialize database
55 createWindow() 45 getDb();
56 app.on( 46
57 'activate', 47 createWindow();
58 () => BrowserWindow.getAllWindows().length === 0 && createWindow() 48
59 ) 49 app.on("activate", () => {
60}) 50 if (BrowserWindow.getAllWindows().length === 0) {
61app.on('window-all-closed', () => { 51 createWindow();
62 if (process.platform !== 'darwin') app.quit() 52 }
63}) 53 });
54});
55
56app.on("window-all-closed", () => {
57 closeDb();
58 if (process.platform !== "darwin") {
59 app.quit();
60 }
61});
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts
new file mode 100644
index 0000000..ce95f4c
--- /dev/null
+++ b/src/main/ipc/handlers.ts
@@ -0,0 +1,117 @@
1import { ipcMain, dialog, type BrowserWindow } from "electron";
2import * as projects from "../db/projects";
3import * as sessions from "../db/sessions";
4import * as claude from "../claude";
5import type { UserPermissionMode } from "../claude/phases";
6
7export function registerIpcHandlers(mainWindow: BrowserWindow): void {
8 // Projects
9 ipcMain.handle("projects:list", () => projects.listProjects());
10 ipcMain.handle("projects:create", (_, name: string, path: string) =>
11 projects.createProject(name, path)
12 );
13 ipcMain.handle("projects:delete", (_, id: string) =>
14 projects.deleteProject(id)
15 );
16
17 // Sessions
18 ipcMain.handle("sessions:list", (_, projectId: string) =>
19 sessions.listSessions(projectId)
20 );
21 ipcMain.handle("sessions:create", (_, projectId: string, name: string) =>
22 sessions.createSession(projectId, name)
23 );
24 ipcMain.handle("sessions:delete", (_, id: string) =>
25 sessions.deleteSession(id)
26 );
27 ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id));
28
29 // Messages
30 ipcMain.handle("messages:list", (_, sessionId: string) =>
31 sessions.listMessages(sessionId)
32 );
33
34 // Chat
35 ipcMain.handle(
36 "chat:send",
37 async (_, sessionId: string, message: string) => {
38 const session = sessions.getSession(sessionId);
39 if (!session) throw new Error("Session not found");
40
41 // Store user message
42 sessions.addMessage(sessionId, "user", message);
43
44 await claude.sendMessage({
45 session,
46 message,
47 onMessage: (msg) => {
48 // Forward all messages to renderer
49 mainWindow.webContents.send("claude:message", sessionId, msg);
50
51 // Store assistant text messages
52 if (msg.type === "assistant") {
53 const content = (msg.message.content as Array<{ type: string; text?: string }>)
54 .filter((c) => c.type === "text" && c.text)
55 .map((c) => c.text!)
56 .join("\n");
57 if (content) {
58 sessions.addMessage(sessionId, "assistant", content);
59 }
60 }
61 },
62 });
63 }
64 );
65
66 ipcMain.handle("chat:interrupt", (_, sessionId: string) => {
67 claude.interruptSession(sessionId);
68 });
69
70 // Workflow
71 ipcMain.handle("workflow:review", async (_, sessionId: string) => {
72 const session = sessions.getSession(sessionId);
73 if (!session) throw new Error("Session not found");
74
75 await claude.triggerReview(session, (msg) => {
76 mainWindow.webContents.send("claude:message", sessionId, msg);
77 });
78 });
79
80 ipcMain.handle("workflow:advance", (_, sessionId: string) => {
81 const session = sessions.getSession(sessionId);
82 if (!session) throw new Error("Session not found");
83 return claude.advancePhase(session);
84 });
85
86 ipcMain.handle(
87 "workflow:setPermissionMode",
88 (_, sessionId: string, mode: string) => {
89 sessions.updateSession(sessionId, {
90 permission_mode: mode as UserPermissionMode,
91 });
92 }
93 );
94
95 // Artifacts
96 ipcMain.handle(
97 "artifact:read",
98 (_, projectPath: string, filename: string) => {
99 return claude.readArtifact(projectPath, filename);
100 }
101 );
102
103 ipcMain.handle(
104 "artifact:write",
105 (_, projectPath: string, filename: string, content: string) => {
106 claude.writeArtifact(projectPath, filename, content);
107 }
108 );
109
110 // Dialogs
111 ipcMain.handle("dialog:selectDirectory", async () => {
112 const result = await dialog.showOpenDialog(mainWindow, {
113 properties: ["openDirectory"],
114 });
115 return result.canceled ? null : result.filePaths[0];
116 });
117}
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 0b39d91..b3e3f8b 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -1 +1,114 @@
1// Expose nothing for now; keep the door open for future IPC-safe APIs. 1import { contextBridge, ipcRenderer, type IpcRendererEvent } from "electron";
2import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
3import type { Project } from "./db/projects";
4import type { Session, Message } from "./db/sessions";
5import type { Phase, UserPermissionMode } from "./claude/phases";
6
7export interface ClaudeFlowAPI {
8 // Projects
9 listProjects: () => Promise<Project[]>;
10 createProject: (name: string, path: string) => Promise<Project>;
11 deleteProject: (id: string) => Promise<void>;
12
13 // Sessions
14 listSessions: (projectId: string) => Promise<Session[]>;
15 createSession: (projectId: string, name: string) => Promise<Session>;
16 deleteSession: (id: string) => Promise<void>;
17 getSession: (id: string) => Promise<Session | undefined>;
18
19 // Messages
20 listMessages: (sessionId: string) => Promise<Message[]>;
21
22 // Chat
23 sendMessage: (sessionId: string, message: string) => Promise<void>;
24 interruptSession: (sessionId: string) => Promise<void>;
25
26 // Workflow
27 triggerReview: (sessionId: string) => Promise<void>;
28 advancePhase: (sessionId: string) => Promise<Phase | null>;
29 setPermissionMode: (
30 sessionId: string,
31 mode: UserPermissionMode
32 ) => Promise<void>;
33
34 // Artifacts
35 readArtifact: (
36 projectPath: string,
37 filename: string
38 ) => Promise<string | null>;
39 writeArtifact: (
40 projectPath: string,
41 filename: string,
42 content: string
43 ) => Promise<void>;
44
45 // Events
46 onClaudeMessage: (
47 callback: (sessionId: string, message: SDKMessage) => void
48 ) => () => void;
49
50 // Dialogs
51 selectDirectory: () => Promise<string | null>;
52}
53
54const api: ClaudeFlowAPI = {
55 // Projects
56 listProjects: () => ipcRenderer.invoke("projects:list"),
57 createProject: (name, path) =>
58 ipcRenderer.invoke("projects:create", name, path),
59 deleteProject: (id) => ipcRenderer.invoke("projects:delete", id),
60
61 // Sessions
62 listSessions: (projectId) => ipcRenderer.invoke("sessions:list", projectId),
63 createSession: (projectId, name) =>
64 ipcRenderer.invoke("sessions:create", projectId, name),
65 deleteSession: (id) => ipcRenderer.invoke("sessions:delete", id),
66 getSession: (id) => ipcRenderer.invoke("sessions:get", id),
67
68 // Messages
69 listMessages: (sessionId) => ipcRenderer.invoke("messages:list", sessionId),
70
71 // Chat
72 sendMessage: (sessionId, message) =>
73 ipcRenderer.invoke("chat:send", sessionId, message),
74 interruptSession: (sessionId) =>
75 ipcRenderer.invoke("chat:interrupt", sessionId),
76
77 // Workflow
78 triggerReview: (sessionId) => ipcRenderer.invoke("workflow:review", sessionId),
79 advancePhase: (sessionId) => ipcRenderer.invoke("workflow:advance", sessionId),
80 setPermissionMode: (sessionId, mode) =>
81 ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode),
82
83 // Artifacts
84 readArtifact: (projectPath, filename) =>
85 ipcRenderer.invoke("artifact:read", projectPath, filename),
86 writeArtifact: (projectPath, filename, content) =>
87 ipcRenderer.invoke("artifact:write", projectPath, filename, content),
88
89 // Events
90 onClaudeMessage: (callback) => {
91 const handler = (
92 _: IpcRendererEvent,
93 sessionId: string,
94 message: SDKMessage
95 ) => callback(sessionId, message);
96 ipcRenderer.on("claude:message", handler);
97 return () => ipcRenderer.removeListener("claude:message", handler);
98 },
99
100 // Dialogs
101 selectDirectory: async () => {
102 const result = await ipcRenderer.invoke("dialog:selectDirectory");
103 return result;
104 },
105};
106
107contextBridge.exposeInMainWorld("api", api);
108
109// Type declaration for renderer
110declare global {
111 interface Window {
112 api: ClaudeFlowAPI;
113 }
114}