aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-28 20:45:23 -0800
committerbndw <ben@bdw.to>2026-02-28 20:45:23 -0800
commit0da42e4fa414ab3268d4f71896455097239f8590 (patch)
tree72e951bdf8b591f4c949c6fd687ef780580c8783
parentdc4156fec54a8efdab84834fe2f5bc90120e32c1 (diff)
feat: Complete 9 tasks
- ✅ **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 `<option>` text - ✅ **Change 5b** — `renderer/src/components/Header.tsx`: Add `phase !== "implement"` guard to rename button - ✅ **Change 5c** — `renderer/src/components/Header.tsx`: Gate badge on `gitBranch` truthy; remove disabled/unavailable state - ✅ **Change 6** — `renderer/src/styles/globals.css`: Delete `.branch-badge.branch-unavailable` rule
-rw-r--r--renderer/src/App.tsx12
-rw-r--r--renderer/src/components/Header.tsx41
-rw-r--r--renderer/src/styles/globals.css6
-rw-r--r--src/main/git.ts86
-rw-r--r--src/main/ipc/handlers.ts30
-rw-r--r--src/main/preload.ts2
6 files changed, 128 insertions, 49 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index ecbb5b2..74b1f91 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -263,13 +263,17 @@ export function App() {
263 selectedSession.phase === "research" ? "research.md" : "plan.md"; 263 selectedSession.phase === "research" ? "research.md" : "plan.md";
264 await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent); 264 await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent);
265 265
266 const nextPhase = await api.advancePhase(selectedSession.id); 266 const advanced = await api.advancePhase(selectedSession.id);
267 if (nextPhase) { 267 if (advanced) {
268 setSelectedSession({ ...selectedSession, phase: nextPhase }); 268 setSelectedSession({
269 ...selectedSession,
270 phase: advanced.phase,
271 git_branch: advanced.git_branch,
272 });
269 // Trigger initial message for next phase 273 // Trigger initial message for next phase
270 setIsLoading(true); 274 setIsLoading(true);
271 const initialMsg = 275 const initialMsg =
272 nextPhase === "plan" 276 advanced.phase === "plan"
273 ? "Create a detailed implementation plan based on the research." 277 ? "Create a detailed implementation plan based on the research."
274 : "Begin implementing the plan."; 278 : "Begin implementing the plan.";
275 await api.sendMessage(selectedSession.id, initialMsg); 279 await api.sendMessage(selectedSession.id, initialMsg);
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx
index e56264f..fc0289d 100644
--- a/renderer/src/components/Header.tsx
+++ b/renderer/src/components/Header.tsx
@@ -152,21 +152,23 @@ export function Header({
152 {sessions.map((s) => ( 152 {sessions.map((s) => (
153 <option key={s.id} value={s.id}> 153 <option key={s.id} value={s.id}>
154 {s.name} 154 {s.name}
155 {s.git_branch ? ` · ${s.git_branch}` : " · git unavailable"}
156 </option> 155 </option>
157 ))} 156 ))}
158 </select> 157 </select>
159 )} 158 )}
160 <button onClick={onCreateSession}>+ Session</button> 159 <button onClick={onCreateSession}>+ Session</button>
161 {selectedSession && onRenameSession && !isRenamingSession && ( 160 {selectedSession &&
162 <button 161 onRenameSession &&
163 onClick={startRename} 162 !isRenamingSession &&
164 className="btn-rename" 163 selectedSession.phase !== "implement" && (
165 title="Rename session" 164 <button
166 > 165 onClick={startRename}
167 ✏️ 166 className="btn-rename"
168 </button> 167 title="Rename session"
169 )} 168 >
169 ✏️
170 </button>
171 )}
170 {selectedSession && onDeleteSession && ( 172 {selectedSession && onDeleteSession && (
171 <button 173 <button
172 onClick={handleDeleteSession} 174 onClick={handleDeleteSession}
@@ -204,26 +206,15 @@ export function Header({
204 )} 206 )}
205 207
206 {/* ── Branch badge ── */} 208 {/* ── Branch badge ── */}
207 {selectedSession && ( 209 {selectedSession && gitBranch && (
208 <button 210 <button
209 className={[ 211 className={["branch-badge", copied ? "branch-copied" : ""]
210 "branch-badge",
211 gitBranch ? "" : "branch-unavailable",
212 copied ? "branch-copied" : "",
213 ]
214 .filter(Boolean) 212 .filter(Boolean)
215 .join(" ")} 213 .join(" ")}
216 onClick={handleCopyBranch} 214 onClick={handleCopyBranch}
217 disabled={!gitBranch} 215 title={copied ? "Copied!" : `Click to copy: ${gitBranch}`}
218 title={
219 gitBranch
220 ? copied
221 ? "Copied!"
222 : `Click to copy: ${gitBranch}`
223 : "Git unavailable for this session"
224 }
225 > 216 >
226 ⎇ {gitBranch ?? "git unavailable"} 217 ⎇ {gitBranch}
227 </button> 218 </button>
228 )} 219 )}
229 220
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 {
206 color: white; 206 color: white;
207} 207}
208 208
209.branch-badge.branch-unavailable {
210 border-style: dashed;
211 opacity: 0.5;
212 cursor: default;
213}
214
215/* ── Main Content ─────────────────────────────────────────────── */ 209/* ── Main Content ─────────────────────────────────────────────── */
216.main-content { 210.main-content {
217 flex: 1; 211 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
@@ -146,6 +146,81 @@ function extractTaskName(checkboxLine: string): string {
146} 146}
147 147
148// --------------------------------------------------------------------------- 148// ---------------------------------------------------------------------------
149// Commit subject builders
150// ---------------------------------------------------------------------------
151
152const 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 */
168function 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 */
190function 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 */
215function 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";
2import * as projects from "../db/projects"; 2import * as projects from "../db/projects";
3import * as sessions from "../db/sessions"; 3import * as sessions from "../db/sessions";
4import * as claude from "../claude"; 4import * as claude from "../claude";
5import { createSessionBranch } from "../git"; 5import { createSessionBranch, ensureGitIgnore } from "../git";
6import type { UserPermissionMode } from "../claude/phases"; 6import type { UserPermissionMode } from "../claude/phases";
7 7
8export function registerIpcHandlers(mainWindow: BrowserWindow): void { 8export 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