diff options
| author | Clawd <ai@clawd.bot> | 2026-02-28 07:32:23 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-28 07:32:23 -0800 |
| commit | 35a38d909f1777e3b384ba26d68b88f641ec6a1a (patch) | |
| tree | 811e5763a5207433896c986e7654cd5bff9ad93e | |
| parent | 6d70c5f8a3ed90564b08616a3fb041409916059c (diff) | |
Phase 5: Integration & polish
- Add error handling with auto-dismissing error bar
- Add keyboard shortcuts:
- Escape to interrupt Claude
- Cmd/Ctrl+Enter to submit
- Fix unused import warning
- Update README.md with project documentation
- Concept and workflow explanation
- UI layout diagram
- Installation and development instructions
- Project structure overview
- Keyboard shortcuts reference
| -rw-r--r-- | README.md | 136 | ||||
| -rw-r--r-- | renderer/src/App.tsx | 106 | ||||
| -rw-r--r-- | renderer/src/styles/globals.css | 24 |
3 files changed, 171 insertions, 95 deletions
| @@ -1,104 +1,98 @@ | |||
| 1 | # minimal-electron-bsql | 1 | # Claude Flow |
| 2 | 2 | ||
| 3 | A bare-bones Electron + TypeScript + [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) app. | 3 | A document-centric coding assistant that enforces a structured workflow: **Research → Plan → Implement**. |
| 4 | No forge, no boilerplate, no scaffolding. Just the absolute minimum setup that: | ||
| 5 | 4 | ||
| 6 | - Runs in dev mode on macOS/Linux/Windows | 5 | Built 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 | 9 | The 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 | |||
| 13 | 1. **Research** — Claude researches your codebase based on your direction, writing findings to `.claude-flow/research.md` | ||
| 14 | 2. **Plan** — Claude creates a detailed implementation plan in `.claude-flow/plan.md` with code snippets and a TODO checklist | ||
| 15 | 3. **Implement** — Claude executes the plan, marking tasks complete as it goes | ||
| 16 | |||
| 17 | At 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 | ``` |
| 17 | minimal-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 |
| 38 | git clone <this-repo> | 49 | git clone <this-repo> |
| 39 | cd minimal-electron-bsql | 50 | cd claude-flow |
| 40 | npm install | 51 | npm install |
| 41 | ``` | 52 | ``` |
| 42 | 53 | ||
| 43 | ℹ️ On install, native modules are rebuilt for your Electron version via `@electron/rebuild`. | 54 | ## Development |
| 44 | |||
| 45 | ⸻ | ||
| 46 | |||
| 47 | Scripts | ||
| 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 | |||
| 58 | In src/main/index.ts: | ||
| 59 | |||
| 60 | 1. Database file created at app.getPath('userData')/app.db | ||
| 61 | 2. A table messages is created (if not exists) | ||
| 62 | 3. One row "hello from better-sqlite3" is inserted | ||
| 63 | 4. A SELECT runs and the row is logged to console | ||
| 64 | 5. Window shows renderer/index.html → just <main>hi</main> | ||
| 65 | |||
| 66 | ⸻ | ||
| 67 | |||
| 68 | Packaging 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 |
| 79 | npm run dist -- --mac arm64 | 57 | npm run dev |
| 80 | npm run dist -- --mac x64 | ||
| 81 | ``` | 58 | ``` |
| 82 | 59 | ||
| 83 | - Optionally merge into a universal binary: | 60 | This starts Vite dev server and Electron in watch mode. |
| 61 | |||
| 62 | ## Build | ||
| 84 | 63 | ||
| 85 | ```bash | 64 | ```bash |
| 86 | npx electron-builder --universal | 65 | npm 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. | 68 | Creates distributable builds via electron-builder. |
| 69 | |||
| 70 | ## Project Structure | ||
| 90 | 71 | ||
| 91 | ⸻ | 72 | ``` |
| 73 | claude-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 | ||
| 93 | Common 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 @@ | |||
| 1 | import React, { useState, useEffect, useCallback } from "react"; | 1 | import React, { useState, useEffect } from "react"; |
| 2 | import { Header } from "./components/Header"; | 2 | import { Header } from "./components/Header"; |
| 3 | import { DocumentPane } from "./components/DocumentPane"; | 3 | import { DocumentPane } from "./components/DocumentPane"; |
| 4 | import { ChatPane } from "./components/ChatPane"; | 4 | import { 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 | } | ||
