diff options
Diffstat (limited to 'src/main')
| -rw-r--r-- | src/main/git.ts | 86 | ||||
| -rw-r--r-- | src/main/ipc/handlers.ts | 30 | ||||
| -rw-r--r-- | src/main/preload.ts | 2 |
3 files changed, 104 insertions, 14 deletions
diff --git a/src/main/git.ts b/src/main/git.ts index 58dc860..20362a7 100644 --- a/src/main/git.ts +++ b/src/main/git.ts | |||
| @@ -146,6 +146,81 @@ function extractTaskName(checkboxLine: string): string { | |||
| 146 | } | 146 | } |
| 147 | 147 | ||
| 148 | // --------------------------------------------------------------------------- | 148 | // --------------------------------------------------------------------------- |
| 149 | // Commit subject builders | ||
| 150 | // --------------------------------------------------------------------------- | ||
| 151 | |||
| 152 | const LOCK_FILES = new Set([ | ||
| 153 | "package-lock.json", | ||
| 154 | "yarn.lock", | ||
| 155 | "pnpm-lock.yaml", | ||
| 156 | "bun.lockb", | ||
| 157 | "Cargo.lock", | ||
| 158 | "Gemfile.lock", | ||
| 159 | "poetry.lock", | ||
| 160 | ]); | ||
| 161 | |||
| 162 | /** | ||
| 163 | * Builds a `feat:` subject from completed task names. | ||
| 164 | * Keeps the total subject ≤ 72 chars. | ||
| 165 | * 1 task → "feat: {name}" | ||
| 166 | * N tasks → "feat: {name} (+N-1 more)" | ||
| 167 | */ | ||
| 168 | function buildTaskSubject(taskNames: string[]): string { | ||
| 169 | const prefix = "feat: "; | ||
| 170 | const MAX = 72; | ||
| 171 | |||
| 172 | if (taskNames.length === 1) { | ||
| 173 | const full = `${prefix}${taskNames[0]}`; | ||
| 174 | if (full.length <= MAX) return full; | ||
| 175 | return `${prefix}${taskNames[0].slice(0, MAX - prefix.length - 1)}\u2026`; | ||
| 176 | } | ||
| 177 | |||
| 178 | const suffix = ` (+${taskNames.length - 1} more)`; | ||
| 179 | const available = MAX - prefix.length - suffix.length; | ||
| 180 | const first = taskNames[0]; | ||
| 181 | const truncated = | ||
| 182 | first.length > available ? `${first.slice(0, available - 1)}\u2026` : first; | ||
| 183 | return `${prefix}${truncated}${suffix}`; | ||
| 184 | } | ||
| 185 | |||
| 186 | /** | ||
| 187 | * Returns the basenames of staged files, excluding lock files. | ||
| 188 | * Returns empty array on any failure. | ||
| 189 | */ | ||
| 190 | function getStagedFileNames(projectPath: string): string[] { | ||
| 191 | try { | ||
| 192 | const raw = execFileSync("git", ["diff", "--cached", "--name-only"], { | ||
| 193 | cwd: projectPath, | ||
| 194 | stdio: "pipe", | ||
| 195 | }) | ||
| 196 | .toString() | ||
| 197 | .trim(); | ||
| 198 | if (!raw) return []; | ||
| 199 | return raw | ||
| 200 | .split("\n") | ||
| 201 | .map((f) => path.basename(f.trim())) | ||
| 202 | .filter((f) => f && !LOCK_FILES.has(f)); | ||
| 203 | } catch { | ||
| 204 | return []; | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * Builds a `chore:` subject from the staged file list when no tasks completed. | ||
| 210 | * 1 file → "chore: Update {file}" | ||
| 211 | * 2 files → "chore: Update {file1}, {file2}" | ||
| 212 | * 3+ files → "chore: Update {file1} (+N more files)" | ||
| 213 | * 0 files → "chore: Implement progress" | ||
| 214 | */ | ||
| 215 | function buildFileSubject(projectPath: string): string { | ||
| 216 | const files = getStagedFileNames(projectPath); | ||
| 217 | if (files.length === 0) return "chore: Implement progress"; | ||
| 218 | if (files.length === 1) return `chore: Update ${files[0]}`; | ||
| 219 | if (files.length === 2) return `chore: Update ${files[0]}, ${files[1]}`; | ||
| 220 | return `chore: Update ${files[0]} (+${files.length - 1} more files)`; | ||
| 221 | } | ||
| 222 | |||
| 223 | // --------------------------------------------------------------------------- | ||
| 149 | // Auto-commit | 224 | // Auto-commit |
| 150 | // --------------------------------------------------------------------------- | 225 | // --------------------------------------------------------------------------- |
| 151 | 226 | ||
| @@ -184,13 +259,12 @@ export function autoCommitTurn( | |||
| 184 | 259 | ||
| 185 | let commitMsg: string; | 260 | let commitMsg: string; |
| 186 | if (newlyCompleted.length > 0) { | 261 | if (newlyCompleted.length > 0) { |
| 187 | const count = newlyCompleted.length; | 262 | const taskNames = newlyCompleted.map(extractTaskName); |
| 188 | const taskLines = newlyCompleted | 263 | const subject = buildTaskSubject(taskNames); |
| 189 | .map((l) => `- ✅ ${extractTaskName(l)}`) | 264 | const body = taskNames.map((t) => `- \u2705 ${t}`).join("\n"); |
| 190 | .join("\n"); | 265 | commitMsg = `${subject}\n\n${body}`; |
| 191 | commitMsg = `feat: Complete ${count} task${count > 1 ? "s" : ""}\n\n${taskLines}`; | ||
| 192 | } else { | 266 | } else { |
| 193 | commitMsg = "chore: Implement progress (no tasks completed this turn)"; | 267 | commitMsg = buildFileSubject(projectPath); |
| 194 | } | 268 | } |
| 195 | 269 | ||
| 196 | execFileSync("git", ["commit", "-m", commitMsg], { cwd: projectPath }); | 270 | execFileSync("git", ["commit", "-m", commitMsg], { cwd: projectPath }); |
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index f0a5b82..774eb63 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts | |||
| @@ -2,7 +2,7 @@ import { ipcMain, dialog, type BrowserWindow } from "electron"; | |||
| 2 | import * as projects from "../db/projects"; | 2 | import * as projects from "../db/projects"; |
| 3 | import * as sessions from "../db/sessions"; | 3 | import * as sessions from "../db/sessions"; |
| 4 | import * as claude from "../claude"; | 4 | import * as claude from "../claude"; |
| 5 | import { createSessionBranch } from "../git"; | 5 | import { createSessionBranch, ensureGitIgnore } from "../git"; |
| 6 | import type { UserPermissionMode } from "../claude/phases"; | 6 | import type { UserPermissionMode } from "../claude/phases"; |
| 7 | 7 | ||
| 8 | export function registerIpcHandlers(mainWindow: BrowserWindow): void { | 8 | export function registerIpcHandlers(mainWindow: BrowserWindow): void { |
| @@ -26,12 +26,11 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { | |||
| 26 | 26 | ||
| 27 | const session = sessions.createSession(projectId, name); | 27 | const session = sessions.createSession(projectId, name); |
| 28 | 28 | ||
| 29 | const branchName = createSessionBranch(project.path, session.name, session.id); | 29 | // Ensure .claude-flow/ is gitignored from day one. |
| 30 | if (branchName) { | 30 | // Branch creation is deferred until the session advances to implement. |
| 31 | sessions.updateSession(session.id, { git_branch: branchName }); | 31 | try { ensureGitIgnore(project.path); } catch { /* non-fatal */ } |
| 32 | } | ||
| 33 | 32 | ||
| 34 | return { ...session, git_branch: branchName ?? null }; | 33 | return session; |
| 35 | }); | 34 | }); |
| 36 | 35 | ||
| 37 | ipcMain.handle("sessions:delete", (_, id: string) => { | 36 | ipcMain.handle("sessions:delete", (_, id: string) => { |
| @@ -103,7 +102,24 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { | |||
| 103 | ipcMain.handle("workflow:advance", (_, sessionId: string) => { | 102 | ipcMain.handle("workflow:advance", (_, sessionId: string) => { |
| 104 | const session = sessions.getSession(sessionId); | 103 | const session = sessions.getSession(sessionId); |
| 105 | if (!session) throw new Error("Session not found"); | 104 | if (!session) throw new Error("Session not found"); |
| 106 | return claude.advancePhase(session); | 105 | |
| 106 | const nextPhase = claude.advancePhase(session); | ||
| 107 | if (!nextPhase) return null; | ||
| 108 | |||
| 109 | let git_branch = session.git_branch; | ||
| 110 | |||
| 111 | if (nextPhase === "implement") { | ||
| 112 | const project = projects.getProject(session.project_id); | ||
| 113 | if (project) { | ||
| 114 | const branchName = createSessionBranch(project.path, session.name, session.id); | ||
| 115 | if (branchName) { | ||
| 116 | sessions.updateSession(sessionId, { git_branch: branchName }); | ||
| 117 | git_branch = branchName; | ||
| 118 | } | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | return { phase: nextPhase, git_branch }; | ||
| 107 | }); | 123 | }); |
| 108 | 124 | ||
| 109 | ipcMain.handle( | 125 | ipcMain.handle( |
diff --git a/src/main/preload.ts b/src/main/preload.ts index f377639..299a1b5 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts | |||
| @@ -26,7 +26,7 @@ export interface ClaudeFlowAPI { | |||
| 26 | 26 | ||
| 27 | // Workflow | 27 | // Workflow |
| 28 | triggerReview: (sessionId: string) => Promise<void>; | 28 | triggerReview: (sessionId: string) => Promise<void>; |
| 29 | advancePhase: (sessionId: string) => Promise<Phase | null>; | 29 | advancePhase: (sessionId: string) => Promise<{ phase: Phase; git_branch: string | null } | null>; |
| 30 | setPermissionMode: ( | 30 | setPermissionMode: ( |
| 31 | sessionId: string, | 31 | sessionId: string, |
| 32 | mode: UserPermissionMode | 32 | mode: UserPermissionMode |
