From 0484d97dfbc3b8a2e7878d3ab35a9895decdf467 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 28 Feb 2026 21:35:39 -0800 Subject: feat: **1 — `git.ts`:** Add exported `getCurrentBranch` helpe… (+8 more) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ **1 — `git.ts`:** Add exported `getCurrentBranch` helper after `ensureGitRepo` - ✅ **2a — `ipc/handlers.ts`:** Update git import to include `ensureGitRepo` and `getCurrentBranch` - ✅ **2b — `ipc/handlers.ts`:** Replace `workflow:advance` implement-phase block with branching-toggle logic - ✅ **3 — `GitSettings.tsx`:** Create new settings component with pill toggle - ✅ **4 — `SettingsPage.tsx`:** Add `"git"` section type, import, nav item, content render; fix both unicode glyphs - ✅ **5 — `globals.css`:** Append toggle-row + pill toggle + maximize-btn CSS - ✅ **6 — `index.ts`:** Add `ipcMain` to import; add `window:toggleMaximize` handler + maximize/unmaximize events inside `createWindow()` - ✅ **7 — `preload.ts`:** Add `toggleMaximize` + `onWindowMaximized` to interface and `api` object - ✅ **8 — `Header.tsx`:** Add `isMaximized` state + effect + maximize button in JSX --- renderer/src/components/Header.tsx | 23 +++++- renderer/src/components/SettingsPage.tsx | 17 ++++- renderer/src/components/settings/GitSettings.tsx | 78 ++++++++++++++++++++ renderer/src/styles/globals.css | 93 ++++++++++++++++++++++++ src/main/git.ts | 23 ++++++ src/main/index.ts | 14 +++- src/main/ipc/handlers.ts | 27 +++++-- src/main/preload.ts | 12 +++ 8 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 renderer/src/components/settings/GitSettings.tsx diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx index 3a530d3..1d954ec 100644 --- a/renderer/src/components/Header.tsx +++ b/renderer/src/components/Header.tsx @@ -1,6 +1,8 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import type { Project, Session, Phase } from "../types"; +const api = window.api; + type Theme = "dark" | "light"; interface HeaderProps { @@ -86,6 +88,16 @@ export function Header({ setIsRenamingSession(false); }; + // ── Maximize ───────────────────────────────────────────────── + const [isMaximized, setIsMaximized] = useState(false); + + useEffect(() => { + // Returns the unsubscribe function; React cleanup calls it on unmount. + // On macOS, clicking the native green traffic light also fires this, + // keeping the glyph accurate when native controls are used. + return api.onWindowMaximized(setIsMaximized); + }, []); + // ── Branch copy ────────────────────────────────────────────── const [copied, setCopied] = useState(false); @@ -225,6 +237,15 @@ export function Header({ {theme === "dark" ? "[light]" : "[dark]"} + {/* ── Maximize toggle ── */} + + {/* ── Settings button ── */} @@ -38,12 +39,20 @@ export function SettingsPage({ onClose }: SettingsPageProps) { > System Prompts - {/* Future sections added here */} + {/* Content */}
{activeSection === "system-prompts" && } + {activeSection === "git" && }
diff --git a/renderer/src/components/settings/GitSettings.tsx b/renderer/src/components/settings/GitSettings.tsx new file mode 100644 index 0000000..0ec9875 --- /dev/null +++ b/renderer/src/components/settings/GitSettings.tsx @@ -0,0 +1,78 @@ +import React, { useState, useEffect } from "react"; + +const api = window.api; + +export function GitSettings() { + // null = setting not yet loaded; true/false = loaded value + const [branchingEnabled, setBranchingEnabled] = useState(null); + const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle"); + + useEffect(() => { + api.getSettings(["git.branchingEnabled"]).then((settings) => { + // Key absent or any value other than "true" → false (opt-in default) + setBranchingEnabled(settings["git.branchingEnabled"] === "true"); + }); + }, []); + + const handleToggle = async (enabled: boolean) => { + setBranchingEnabled(enabled); + if (enabled) { + await api.setSetting("git.branchingEnabled", "true"); + } else { + await api.deleteSetting("git.branchingEnabled"); + } + setSaveStatus("saved"); + setTimeout(() => setSaveStatus("idle"), 1500); + }; + + if (branchingEnabled === null) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
Git
+
+ Configure how Claude Flow interacts with your project's git repository + during the implement phase. +
+ +
+ + {saveStatus === "saved" && ( + + saved ✓ + + )} +
+
+ ); +} diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 61a37c0..8e43f3a 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css @@ -905,3 +905,96 @@ html[data-theme="light"] .settings-textarea:focus { background: var(--bg-tertiary); color: var(--text-primary); } + +/* ── Settings Toggle Row ─────────────────────────────────────── */ +.settings-toggle-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 16px; +} + +.settings-toggle-label { + display: flex; + align-items: flex-start; + gap: 14px; + cursor: pointer; + flex: 1; +} + +.settings-toggle-text { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + color: var(--text-primary); + line-height: 1.4; +} + +.settings-toggle-hint { + color: var(--text-secondary); + font-size: 11px; +} + +.settings-toggle-hint code { + font-family: monospace; + background: var(--bg-tertiary); + padding: 1px 3px; + border-radius: 2px; +} + +/* The pill toggle switch */ +.settings-toggle { + flex-shrink: 0; + width: 36px; + height: 20px; + border-radius: 10px; + background: var(--border); + position: relative; + cursor: pointer; + transition: background 0.2s; + outline-offset: 2px; +} + +.settings-toggle:focus-visible { + outline: 2px solid var(--accent); +} + +.settings-toggle::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--text-secondary); + transition: transform 0.2s, background 0.2s; +} + +.settings-toggle.on { + background: var(--accent); +} + +.settings-toggle.on::after { + transform: translateX(16px); + background: #fff; +} + +/* ── Header Maximize Button ──────────────────────────────────── */ +.maximize-btn { + padding: 5px 8px; + background: transparent; + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + font-family: inherit; + transition: background 0.15s, color 0.15s; +} + +.maximize-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} diff --git a/src/main/git.ts b/src/main/git.ts index 20362a7..ec81e8f 100644 --- a/src/main/git.ts +++ b/src/main/git.ts @@ -57,6 +57,29 @@ export function ensureGitRepo(projectPath: string): void { } } +// --------------------------------------------------------------------------- +// Current branch query +// --------------------------------------------------------------------------- + +/** + * Returns the name of the currently checked-out branch, + * or null if git is unavailable or HEAD is detached. + */ +export function getCurrentBranch(projectPath: string): string | null { + try { + return ( + execFileSync("git", ["branch", "--show-current"], { + cwd: projectPath, + stdio: "pipe", + }) + .toString() + .trim() || null + ); + } catch { + return null; + } +} + // --------------------------------------------------------------------------- // Branch creation // --------------------------------------------------------------------------- diff --git a/src/main/index.ts b/src/main/index.ts index a7bed00..f913ac0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, Menu } from "electron"; +import { app, BrowserWindow, Menu, ipcMain } from "electron"; import path from "node:path"; import { getDb, closeDb } from "./db"; import { registerIpcHandlers } from "./ipc/handlers"; @@ -23,6 +23,18 @@ function createWindow() { registerIpcHandlers(mainWindow); + // Maximize toggle — works identically on Linux and macOS + ipcMain.handle("window:toggleMaximize", () => { + if (mainWindow!.isMaximized()) mainWindow!.unmaximize(); + else mainWindow!.maximize(); + }); + + // Push state to renderer so the button glyph stays accurate. + // On macOS, clicking the green traffic light also fires these events, + // keeping our custom button in sync with the native control. + mainWindow.on("maximize", () => mainWindow!.webContents.send("window:maximized", true)); + mainWindow.on("unmaximize", () => mainWindow!.webContents.send("window:maximized", false)); + if (isDev) { const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173"; mainWindow.loadURL(url).finally(() => { diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index bc7d78d..e0863f3 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -3,7 +3,7 @@ import * as projects from "../db/projects"; import * as sessions from "../db/sessions"; import * as claude from "../claude"; import * as settingsDb from "../db/settings"; -import { createSessionBranch, ensureGitIgnore } from "../git"; +import { createSessionBranch, ensureGitIgnore, ensureGitRepo, getCurrentBranch } from "../git"; import type { UserPermissionMode } from "../claude/phases"; import { getDefaultSystemPromptTemplate } from "../claude/phases"; @@ -113,10 +113,27 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { 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; + const branchingSetting = settingsDb.getSetting("git.branchingEnabled"); + const branchingEnabled = branchingSetting === "true"; // opt-in; default = off + + // Always ensure repo + gitignore so commits work regardless of mode + try { ensureGitIgnore(project.path); } catch { /* non-fatal */ } + try { ensureGitRepo(project.path); } catch { /* non-fatal */ } + + if (branchingEnabled) { + // createSessionBranch internally calls ensureGitIgnore/ensureGitRepo again + // (belt-and-suspenders), then checks out a new claude-flow/- branch + const branchName = createSessionBranch(project.path, session.name, session.id); + if (branchName) { + sessions.updateSession(sessionId, { git_branch: branchName }); + git_branch = branchName; + } + } else { + // No new branch — commit to whatever branch is currently checked out. + // Store the branch name so autoCommitTurn's boolean guard passes. + const currentBranch = getCurrentBranch(project.path) ?? "main"; + sessions.updateSession(sessionId, { git_branch: currentBranch }); + git_branch = currentBranch; } } } diff --git a/src/main/preload.ts b/src/main/preload.ts index 52e947b..44467db 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -62,6 +62,10 @@ export interface ClaudeFlowAPI { // Dialogs selectDirectory: () => Promise; + + // Window + toggleMaximize: () => Promise; + onWindowMaximized: (cb: (isMaximized: boolean) => void) => () => void; } const api: ClaudeFlowAPI = { @@ -127,6 +131,14 @@ const api: ClaudeFlowAPI = { const result = await ipcRenderer.invoke("dialog:selectDirectory"); return result; }, + + // Window + toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"), + onWindowMaximized: (cb) => { + const handler = (_: IpcRendererEvent, val: boolean) => cb(val); + ipcRenderer.on("window:maximized", handler); + return () => ipcRenderer.removeListener("window:maximized", handler); + }, }; contextBridge.exposeInMainWorld("api", api); -- cgit v1.2.3