aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md136
-rw-r--r--renderer/src/App.tsx106
-rw-r--r--renderer/src/styles/globals.css24
3 files changed, 171 insertions, 95 deletions
diff --git a/README.md b/README.md
index f74d4fc..1e23bc9 100644
--- a/README.md
+++ b/README.md
@@ -1,104 +1,98 @@
1# minimal-electron-bsql 1# Claude Flow
2 2
3A bare-bones Electron + TypeScript + [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) app. 3A document-centric coding assistant that enforces a structured workflow: **Research → Plan → Implement**.
4No forge, no boilerplate, no scaffolding. Just the absolute minimum setup that:
5 4
6- Runs in dev mode on macOS/Linux/Windows 5Built with Electron, React, TypeScript, better-sqlite3, and the Claude Agent SDK.
7- Uses `better-sqlite3` from the **main process**
8- Creates a SQLite DB at startup, inserts + selects a row
9- Displays a trivial HTML UI
10- Can be packaged for distribution (SQLite included)
11 6
12--- 7## Concept
13 8
14## Project structure 9The primary UI is a **markdown document viewer/editor** with a **chat sidebar**. The workflow is driven by the document, not the chat.
10
11### Workflow Phases
12
131. **Research** — Claude researches your codebase based on your direction, writing findings to `.claude-flow/research.md`
142. **Plan** — Claude creates a detailed implementation plan in `.claude-flow/plan.md` with code snippets and a TODO checklist
153. **Implement** — Claude executes the plan, marking tasks complete as it goes
16
17At each phase, you can edit the document to add notes (marked with `// REVIEW:` or `// NOTE:`), click **Review** to have Claude address your feedback, then **Submit** to advance to the next phase.
18
19## UI Layout
15 20
16``` 21```
17minimal-electron-bsql/ 22┌──────────────────────────────────────────────────────────────┐
18├── src/ 23│ [Project ▾] [Session ▾] [Research ● ─ Plan ─] │
19│ └── main/ 24├────────────────────────────────────────┬─────────────────────┤
20│ └── index.ts # Electron main process 25│ │ │
21├── renderer/ 26│ # Research Findings │ Chat Dialogue │
22│ └── index.html # Minimal UI 27│ │ │
23├── package.json 28│ ## Authentication System │ Claude: What │
24├── tsconfig.json 29│ │ should I research?│
30│ The auth module uses JWT... │ │
31│ │ You: Research the │
32│ // REVIEW: check OAuth too │ auth system │
33│ │ │
34├────────────────────────────────────────┼─────────────────────┤
35│ 42k / 200k tokens ████░░░░ │ [Send] │
36│ [Review] [Submit] │ │
37└────────────────────────────────────────┴─────────────────────┘
25``` 38```
26 39
27## Requirements 40## Requirements
28 41
29- Node.js ≥ 18 42- Node.js ≥ 18
30- macOS (Apple Silicon or Intel). Windows & Linux should work with native build tools installed. 43- macOS (Apple Silicon or Intel), Windows, or Linux
31- Xcode Command Line Tools (macOS) or MSVC Build Tools (Windows) for native rebuilds 44- Claude API key or Claude Code subscription
32
33---
34 45
35## Install 46## Install
36 47
37```bash 48```bash
38git clone <this-repo> 49git clone <this-repo>
39cd minimal-electron-bsql 50cd claude-flow
40npm install 51npm install
41``` 52```
42 53
43ℹ️ On install, native modules are rebuilt for your Electron version via `@electron/rebuild`. 54## Development
44
45
46
47Scripts
48
49- npm run dev → compile TS and start Electron in dev mode
50- npm run build → compile TypeScript only
51- npm run start → start Electron with compiled code
52- npm run rebuild → force-rebuild native modules (better-sqlite3)
53- npm run dist → create distributable builds via electron-builder
54- npm run pack → package into unpacked app directory
55
56## What happens on startup
57
58In src/main/index.ts:
59
601. Database file created at app.getPath('userData')/app.db
612. A table messages is created (if not exists)
623. One row "hello from better-sqlite3" is inserted
634. A SELECT runs and the row is logged to console
645. Window shows renderer/index.html → just <main>hi</main>
65
66
67
68Packaging notes
69• Native modules: better-sqlite3.node must live outside app.asar. This is handled via:
70
71```
72"asarUnpack": ["node_modules/better-sqlite3/**/*"]
73```
74
75- Cross-arch builds (macOS):
76- Build separately for arm64 and x64:
77 55
78```bash 56```bash
79npm run dist -- --mac arm64 57npm run dev
80npm run dist -- --mac x64
81``` 58```
82 59
83- Optionally merge into a universal binary: 60This starts Vite dev server and Electron in watch mode.
61
62## Build
84 63
85```bash 64```bash
86npx electron-builder --universal 65npm run dist
87``` 66```
88 67
89- Database location: Always use app.getPath('userData'). Do not write next to your code files, since those are inside app.asar when packaged. 68Creates distributable builds via electron-builder.
69
70## Project Structure
90 71
91 72```
73claude-flow/
74├── src/main/ # Electron main process
75│ ├── index.ts # App lifecycle, window management
76│ ├── preload.ts # IPC bridge with typed API
77│ ├── db/ # SQLite database (projects, sessions, messages)
78│ ├── claude/ # Claude SDK wrapper with phase configs
79│ └── ipc/ # IPC handlers
80├── renderer/ # React frontend
81│ ├── src/
82│ │ ├── App.tsx # Main app with state management
83│ │ ├── components/ # Header, DocumentPane, ChatPane, ActionBar
84│ │ ├── styles/ # CSS
85│ │ └── types.ts # TypeScript types
86│ └── index.html
87├── package.json
88├── tsconfig.json
89└── vite.config.ts
90```
92 91
93Common pitfalls & fixes 92## Keyboard Shortcuts
94 93
95- was compiled against a different Node.js version error 94- **Cmd/Ctrl + Enter** — Submit (advance to next phase)
96- Run npm run rebuild after npm install or upgrading Electron. 95- **Escape** — Interrupt Claude
97- .node module not found after packaging
98- Check asarUnpack includes better-sqlite3.
99- Windows/Linux builds
100- Windows: install Build Tools for Visual Studio + Python
101- Linux: sudo apt install build-essential python3
102 96
103## License 97## License
104 98
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index 7a8c378..22082a2 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -1,4 +1,4 @@
1import React, { useState, useEffect, useCallback } from "react"; 1import React, { useState, useEffect } from "react";
2import { Header } from "./components/Header"; 2import { Header } from "./components/Header";
3import { DocumentPane } from "./components/DocumentPane"; 3import { DocumentPane } from "./components/DocumentPane";
4import { ChatPane } from "./components/ChatPane"; 4import { ChatPane } from "./components/ChatPane";
@@ -21,9 +21,42 @@ export function App() {
21 inputTokens: 0, 21 inputTokens: 0,
22 outputTokens: 0, 22 outputTokens: 0,
23 }); 23 });
24 const [error, setError] = useState<string | null>(null);
24 25
25 const hasChanges = documentContent !== originalContent; 26 const hasChanges = documentContent !== originalContent;
26 27
28 // Clear error after 5 seconds
29 useEffect(() => {
30 if (error) {
31 const timer = setTimeout(() => setError(null), 5000);
32 return () => clearTimeout(timer);
33 }
34 }, [error]);
35
36 // Keyboard shortcuts
37 useEffect(() => {
38 const handleKeyDown = (e: KeyboardEvent) => {
39 // Escape to interrupt
40 if (e.key === "Escape" && isLoading && selectedSession) {
41 api.interruptSession(selectedSession.id);
42 setIsLoading(false);
43 }
44 // Cmd/Ctrl + Enter to submit
45 if (
46 e.key === "Enter" &&
47 (e.metaKey || e.ctrlKey) &&
48 selectedSession &&
49 selectedSession.phase !== "implement" &&
50 !isLoading
51 ) {
52 e.preventDefault();
53 handleSubmit();
54 }
55 };
56 window.addEventListener("keydown", handleKeyDown);
57 return () => window.removeEventListener("keydown", handleKeyDown);
58 }, [selectedSession, isLoading]);
59
27 // Load projects on mount 60 // Load projects on mount
28 useEffect(() => { 61 useEffect(() => {
29 api.listProjects().then(setProjects); 62 api.listProjects().then(setProjects);
@@ -122,6 +155,7 @@ export function App() {
122 const handleSendMessage = async (message: string) => { 155 const handleSendMessage = async (message: string) => {
123 if (!selectedSession) return; 156 if (!selectedSession) return;
124 setIsLoading(true); 157 setIsLoading(true);
158 setError(null);
125 setMessages((prev) => [ 159 setMessages((prev) => [
126 ...prev, 160 ...prev,
127 { 161 {
@@ -132,37 +166,54 @@ export function App() {
132 created_at: Date.now() / 1000, 166 created_at: Date.now() / 1000,
133 }, 167 },
134 ]); 168 ]);
135 await api.sendMessage(selectedSession.id, message); 169 try {
170 await api.sendMessage(selectedSession.id, message);
171 } catch (err) {
172 setError(err instanceof Error ? err.message : "Failed to send message");
173 setIsLoading(false);
174 }
136 }; 175 };
137 176
138 const handleReview = async () => { 177 const handleReview = async () => {
139 if (!selectedSession || !selectedProject) return; 178 if (!selectedSession || !selectedProject) return;
140 // Save user edits first 179 setError(null);
141 const filename = 180 try {
142 selectedSession.phase === "research" ? "research.md" : "plan.md"; 181 // Save user edits first
143 await api.writeArtifact(selectedProject.path, filename, documentContent); 182 const filename =
144 setOriginalContent(documentContent); 183 selectedSession.phase === "research" ? "research.md" : "plan.md";
145 setIsLoading(true); 184 await api.writeArtifact(selectedProject.path, filename, documentContent);
146 await api.triggerReview(selectedSession.id); 185 setOriginalContent(documentContent);
186 setIsLoading(true);
187 await api.triggerReview(selectedSession.id);
188 } catch (err) {
189 setError(err instanceof Error ? err.message : "Review failed");
190 setIsLoading(false);
191 }
147 }; 192 };
148 193
149 const handleSubmit = async () => { 194 const handleSubmit = async () => {
150 if (!selectedSession || !selectedProject) return; 195 if (!selectedSession || !selectedProject) return;
151 // Save any pending edits 196 setError(null);
152 const filename = 197 try {
153 selectedSession.phase === "research" ? "research.md" : "plan.md"; 198 // Save any pending edits
154 await api.writeArtifact(selectedProject.path, filename, documentContent); 199 const filename =
155 200 selectedSession.phase === "research" ? "research.md" : "plan.md";
156 const nextPhase = await api.advancePhase(selectedSession.id); 201 await api.writeArtifact(selectedProject.path, filename, documentContent);
157 if (nextPhase) { 202
158 setSelectedSession({ ...selectedSession, phase: nextPhase }); 203 const nextPhase = await api.advancePhase(selectedSession.id);
159 // Trigger initial message for next phase 204 if (nextPhase) {
160 setIsLoading(true); 205 setSelectedSession({ ...selectedSession, phase: nextPhase });
161 const initialMsg = 206 // Trigger initial message for next phase
162 nextPhase === "plan" 207 setIsLoading(true);
163 ? "Create a detailed implementation plan based on the research." 208 const initialMsg =
164 : "Begin implementing the plan."; 209 nextPhase === "plan"
165 await api.sendMessage(selectedSession.id, initialMsg); 210 ? "Create a detailed implementation plan based on the research."
211 : "Begin implementing the plan.";
212 await api.sendMessage(selectedSession.id, initialMsg);
213 }
214 } catch (err) {
215 setError(err instanceof Error ? err.message : "Submit failed");
216 setIsLoading(false);
166 } 217 }
167 }; 218 };
168 219
@@ -220,6 +271,13 @@ export function App() {
220 /> 271 />
221 </div> 272 </div>
222 273
274 {error && (
275 <div className="error-bar">
276 <span>⚠️ {error}</span>
277 <button onClick={() => setError(null)}>×</button>
278 </div>
279 )}
280
223 <ActionBar 281 <ActionBar
224 phase={selectedSession?.phase || "research"} 282 phase={selectedSession?.phase || "research"}
225 hasChanges={hasChanges} 283 hasChanges={hasChanges}
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index 57dc917..d917f7e 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -389,3 +389,27 @@ body {
389 color: var(--success); 389 color: var(--success);
390 font-size: 14px; 390 font-size: 14px;
391} 391}
392
393/* Error Bar */
394.error-bar {
395 display: flex;
396 justify-content: space-between;
397 align-items: center;
398 padding: 8px 16px;
399 background: var(--danger);
400 color: white;
401 font-size: 14px;
402}
403
404.error-bar button {
405 background: none;
406 border: none;
407 color: white;
408 font-size: 18px;
409 cursor: pointer;
410 padding: 0 4px;
411}
412
413.error-bar button:hover {
414 opacity: 0.8;
415}