From 0da42e4fa414ab3268d4f71896455097239f8590 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 28 Feb 2026 20:45:23 -0800 Subject: feat: Complete 9 tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ **Change 1** — `src/main/git.ts`: Add `LOCK_FILES`, `buildTaskSubject`, `getStagedFileNames`, `buildFileSubject` helpers; rewrite `commitMsg` block in `autoCommitTurn` - ✅ **Change 2a** — `src/main/ipc/handlers.ts`: Update import to include `ensureGitIgnore`; strip branch creation from `sessions:create`; add bare `ensureGitIgnore` call - ✅ **Change 2b** — `src/main/ipc/handlers.ts`: Update `workflow:advance` to create branch on implement transition; return `{ phase, git_branch }` - ✅ **Change 3** — `src/main/preload.ts`: Update `advancePhase` return type in `ClaudeFlowAPI` interface - ✅ **Change 4** — `renderer/src/App.tsx`: Destructure `{ phase, git_branch }` from advance result; spread `git_branch` into `setSelectedSession` - ✅ **Change 5a** — `renderer/src/components/Header.tsx`: Remove branch from ` ))} )} - {selectedSession && onRenameSession && !isRenamingSession && ( - - )} + {selectedSession && + onRenameSession && + !isRenamingSession && + selectedSession.phase !== "implement" && ( + + )} {selectedSession && onDeleteSession && ( )} diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 9d37742..ef0275e 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css @@ -206,12 +206,6 @@ html[data-theme="light"] .session-rename-input { color: white; } -.branch-badge.branch-unavailable { - border-style: dashed; - opacity: 0.5; - cursor: default; -} - /* ── Main Content ─────────────────────────────────────────────── */ .main-content { flex: 1; 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 @@ -145,6 +145,81 @@ function extractTaskName(checkboxLine: string): string { return checkboxLine.replace(/^\s*-\s*\[[xX]\]\s*/, "").trim(); } +// --------------------------------------------------------------------------- +// Commit subject builders +// --------------------------------------------------------------------------- + +const LOCK_FILES = new Set([ + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "bun.lockb", + "Cargo.lock", + "Gemfile.lock", + "poetry.lock", +]); + +/** + * Builds a `feat:` subject from completed task names. + * Keeps the total subject ≤ 72 chars. + * 1 task → "feat: {name}" + * N tasks → "feat: {name} (+N-1 more)" + */ +function buildTaskSubject(taskNames: string[]): string { + const prefix = "feat: "; + const MAX = 72; + + if (taskNames.length === 1) { + const full = `${prefix}${taskNames[0]}`; + if (full.length <= MAX) return full; + return `${prefix}${taskNames[0].slice(0, MAX - prefix.length - 1)}\u2026`; + } + + const suffix = ` (+${taskNames.length - 1} more)`; + const available = MAX - prefix.length - suffix.length; + const first = taskNames[0]; + const truncated = + first.length > available ? `${first.slice(0, available - 1)}\u2026` : first; + return `${prefix}${truncated}${suffix}`; +} + +/** + * Returns the basenames of staged files, excluding lock files. + * Returns empty array on any failure. + */ +function getStagedFileNames(projectPath: string): string[] { + try { + const raw = execFileSync("git", ["diff", "--cached", "--name-only"], { + cwd: projectPath, + stdio: "pipe", + }) + .toString() + .trim(); + if (!raw) return []; + return raw + .split("\n") + .map((f) => path.basename(f.trim())) + .filter((f) => f && !LOCK_FILES.has(f)); + } catch { + return []; + } +} + +/** + * Builds a `chore:` subject from the staged file list when no tasks completed. + * 1 file → "chore: Update {file}" + * 2 files → "chore: Update {file1}, {file2}" + * 3+ files → "chore: Update {file1} (+N more files)" + * 0 files → "chore: Implement progress" + */ +function buildFileSubject(projectPath: string): string { + const files = getStagedFileNames(projectPath); + if (files.length === 0) return "chore: Implement progress"; + if (files.length === 1) return `chore: Update ${files[0]}`; + if (files.length === 2) return `chore: Update ${files[0]}, ${files[1]}`; + return `chore: Update ${files[0]} (+${files.length - 1} more files)`; +} + // --------------------------------------------------------------------------- // Auto-commit // --------------------------------------------------------------------------- @@ -184,13 +259,12 @@ export function autoCommitTurn( let commitMsg: string; if (newlyCompleted.length > 0) { - const count = newlyCompleted.length; - const taskLines = newlyCompleted - .map((l) => `- ✅ ${extractTaskName(l)}`) - .join("\n"); - commitMsg = `feat: Complete ${count} task${count > 1 ? "s" : ""}\n\n${taskLines}`; + const taskNames = newlyCompleted.map(extractTaskName); + const subject = buildTaskSubject(taskNames); + const body = taskNames.map((t) => `- \u2705 ${t}`).join("\n"); + commitMsg = `${subject}\n\n${body}`; } else { - commitMsg = "chore: Implement progress (no tasks completed this turn)"; + commitMsg = buildFileSubject(projectPath); } 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"; import * as projects from "../db/projects"; import * as sessions from "../db/sessions"; import * as claude from "../claude"; -import { createSessionBranch } from "../git"; +import { createSessionBranch, ensureGitIgnore } from "../git"; import type { UserPermissionMode } from "../claude/phases"; export function registerIpcHandlers(mainWindow: BrowserWindow): void { @@ -26,12 +26,11 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { const session = sessions.createSession(projectId, name); - const branchName = createSessionBranch(project.path, session.name, session.id); - if (branchName) { - sessions.updateSession(session.id, { git_branch: branchName }); - } + // Ensure .claude-flow/ is gitignored from day one. + // Branch creation is deferred until the session advances to implement. + try { ensureGitIgnore(project.path); } catch { /* non-fatal */ } - return { ...session, git_branch: branchName ?? null }; + return session; }); ipcMain.handle("sessions:delete", (_, id: string) => { @@ -103,7 +102,24 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { ipcMain.handle("workflow:advance", (_, sessionId: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); - return claude.advancePhase(session); + + const nextPhase = claude.advancePhase(session); + if (!nextPhase) return null; + + let git_branch = session.git_branch; + + if (nextPhase === "implement") { + const project = projects.getProject(session.project_id); + if (project) { + const branchName = createSessionBranch(project.path, session.name, session.id); + if (branchName) { + sessions.updateSession(sessionId, { git_branch: branchName }); + git_branch = branchName; + } + } + } + + return { phase: nextPhase, git_branch }; }); 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 { // Workflow triggerReview: (sessionId: string) => Promise; - advancePhase: (sessionId: string) => Promise; + advancePhase: (sessionId: string) => Promise<{ phase: Phase; git_branch: string | null } | null>; setPermissionMode: ( sessionId: string, mode: UserPermissionMode -- cgit v1.2.3