aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--renderer/src/components/Header.tsx23
-rw-r--r--renderer/src/components/SettingsPage.tsx17
-rw-r--r--renderer/src/components/settings/GitSettings.tsx78
-rw-r--r--renderer/src/styles/globals.css93
-rw-r--r--src/main/git.ts23
-rw-r--r--src/main/index.ts14
-rw-r--r--src/main/ipc/handlers.ts27
-rw-r--r--src/main/preload.ts12
8 files changed, 276 insertions, 11 deletions
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 @@
1import React, { useState } from "react"; 1import React, { useState, useEffect } from "react";
2import type { Project, Session, Phase } from "../types"; 2import type { Project, Session, Phase } from "../types";
3 3
4const api = window.api;
5
4type Theme = "dark" | "light"; 6type Theme = "dark" | "light";
5 7
6interface HeaderProps { 8interface HeaderProps {
@@ -86,6 +88,16 @@ export function Header({
86 setIsRenamingSession(false); 88 setIsRenamingSession(false);
87 }; 89 };
88 90
91 // ── Maximize ─────────────────────────────────────────────────
92 const [isMaximized, setIsMaximized] = useState(false);
93
94 useEffect(() => {
95 // Returns the unsubscribe function; React cleanup calls it on unmount.
96 // On macOS, clicking the native green traffic light also fires this,
97 // keeping the glyph accurate when native controls are used.
98 return api.onWindowMaximized(setIsMaximized);
99 }, []);
100
89 // ── Branch copy ────────────────────────────────────────────── 101 // ── Branch copy ──────────────────────────────────────────────
90 const [copied, setCopied] = useState(false); 102 const [copied, setCopied] = useState(false);
91 103
@@ -225,6 +237,15 @@ export function Header({
225 {theme === "dark" ? "[light]" : "[dark]"} 237 {theme === "dark" ? "[light]" : "[dark]"}
226 </button> 238 </button>
227 239
240 {/* ── Maximize toggle ── */}
241 <button
242 className="maximize-btn"
243 onClick={() => api.toggleMaximize()}
244 title={isMaximized ? "Restore window" : "Maximize window"}
245 >
246 {isMaximized ? '⊡' : '□'}
247 </button>
248
228 {/* ── Settings button ── */} 249 {/* ── Settings button ── */}
229 <button className="settings-btn" onClick={onOpenSettings} title="Settings"> 250 <button className="settings-btn" onClick={onOpenSettings} title="Settings">
230 &#9881; 251 &#9881;
diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx
index 5267665..9ebde44 100644
--- a/renderer/src/components/SettingsPage.tsx
+++ b/renderer/src/components/SettingsPage.tsx
@@ -1,7 +1,8 @@
1import React, { useState } from "react"; 1import React, { useState } from "react";
2import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; 2import { SystemPromptsSettings } from "./settings/SystemPromptsSettings";
3import { GitSettings } from "./settings/GitSettings";
3 4
4type SettingsSection = "system-prompts"; 5type SettingsSection = "system-prompts" | "git";
5 6
6interface SettingsPageProps { 7interface SettingsPageProps {
7 onClose: () => void; 8 onClose: () => void;
@@ -16,14 +17,14 @@ export function SettingsPage({ onClose }: SettingsPageProps) {
16 {/* Header — matches the main app header height/style */} 17 {/* Header — matches the main app header height/style */}
17 <div className="settings-header"> 18 <div className="settings-header">
18 <div className="settings-header-left"> 19 <div className="settings-header-left">
19 <span className="settings-title">\u2699 Settings</span> 20 <span className="settings-title">{'⚙'} Settings</span>
20 </div> 21 </div>
21 <button 22 <button
22 className="settings-close" 23 className="settings-close"
23 onClick={onClose} 24 onClick={onClose}
24 title="Close settings" 25 title="Close settings"
25 > 26 >
26 \u00d7 27 {'×'}
27 </button> 28 </button>
28 </div> 29 </div>
29 30
@@ -38,12 +39,20 @@ export function SettingsPage({ onClose }: SettingsPageProps) {
38 > 39 >
39 System Prompts 40 System Prompts
40 </button> 41 </button>
41 {/* Future sections added here */} 42 <button
43 className={`settings-nav-item${
44 activeSection === "git" ? " active" : ""
45 }`}
46 onClick={() => setActiveSection("git")}
47 >
48 Git
49 </button>
42 </nav> 50 </nav>
43 51
44 {/* Content */} 52 {/* Content */}
45 <div className="settings-content"> 53 <div className="settings-content">
46 {activeSection === "system-prompts" && <SystemPromptsSettings />} 54 {activeSection === "system-prompts" && <SystemPromptsSettings />}
55 {activeSection === "git" && <GitSettings />}
47 </div> 56 </div>
48 </div> 57 </div>
49 </div> 58 </div>
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 @@
1import React, { useState, useEffect } from "react";
2
3const api = window.api;
4
5export function GitSettings() {
6 // null = setting not yet loaded; true/false = loaded value
7 const [branchingEnabled, setBranchingEnabled] = useState<boolean | null>(null);
8 const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
9
10 useEffect(() => {
11 api.getSettings(["git.branchingEnabled"]).then((settings) => {
12 // Key absent or any value other than "true" → false (opt-in default)
13 setBranchingEnabled(settings["git.branchingEnabled"] === "true");
14 });
15 }, []);
16
17 const handleToggle = async (enabled: boolean) => {
18 setBranchingEnabled(enabled);
19 if (enabled) {
20 await api.setSetting("git.branchingEnabled", "true");
21 } else {
22 await api.deleteSetting("git.branchingEnabled");
23 }
24 setSaveStatus("saved");
25 setTimeout(() => setSaveStatus("idle"), 1500);
26 };
27
28 if (branchingEnabled === null) {
29 return (
30 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
31 Loading...
32 </div>
33 );
34 }
35
36 return (
37 <div>
38 <div className="settings-section-title">Git</div>
39 <div className="settings-section-desc">
40 Configure how Claude Flow interacts with your project's git repository
41 during the implement phase.
42 </div>
43
44 <div className="settings-toggle-row">
45 <label className="settings-toggle-label" htmlFor="git-branching-toggle">
46 <span className="settings-toggle-text">
47 <strong>Create a branch per session</strong>
48 <span className="settings-toggle-hint">
49 When enabled, Claude Flow checks out a new{" "}
50 <code>claude-flow/&lt;name&gt;-&lt;id&gt;</code> branch when
51 entering the implement phase. When disabled, commits land on
52 your current branch.
53 </span>
54 </span>
55 <span
56 className={`settings-toggle${branchingEnabled ? " on" : ""}`}
57 role="switch"
58 aria-checked={branchingEnabled}
59 id="git-branching-toggle"
60 tabIndex={0}
61 onClick={() => handleToggle(!branchingEnabled)}
62 onKeyDown={(e) => {
63 if (e.key === " " || e.key === "Enter") {
64 e.preventDefault();
65 handleToggle(!branchingEnabled);
66 }
67 }}
68 />
69 </label>
70 {saveStatus === "saved" && (
71 <span className="settings-custom-badge" style={{ color: "var(--accent)" }}>
72 saved ✓
73 </span>
74 )}
75 </div>
76 </div>
77 );
78}
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 {
905 background: var(--bg-tertiary); 905 background: var(--bg-tertiary);
906 color: var(--text-primary); 906 color: var(--text-primary);
907} 907}
908
909/* ── Settings Toggle Row ─────────────────────────────────────── */
910.settings-toggle-row {
911 display: flex;
912 align-items: center;
913 gap: 12px;
914 margin-top: 16px;
915}
916
917.settings-toggle-label {
918 display: flex;
919 align-items: flex-start;
920 gap: 14px;
921 cursor: pointer;
922 flex: 1;
923}
924
925.settings-toggle-text {
926 display: flex;
927 flex-direction: column;
928 gap: 4px;
929 font-size: 12px;
930 color: var(--text-primary);
931 line-height: 1.4;
932}
933
934.settings-toggle-hint {
935 color: var(--text-secondary);
936 font-size: 11px;
937}
938
939.settings-toggle-hint code {
940 font-family: monospace;
941 background: var(--bg-tertiary);
942 padding: 1px 3px;
943 border-radius: 2px;
944}
945
946/* The pill toggle switch */
947.settings-toggle {
948 flex-shrink: 0;
949 width: 36px;
950 height: 20px;
951 border-radius: 10px;
952 background: var(--border);
953 position: relative;
954 cursor: pointer;
955 transition: background 0.2s;
956 outline-offset: 2px;
957}
958
959.settings-toggle:focus-visible {
960 outline: 2px solid var(--accent);
961}
962
963.settings-toggle::after {
964 content: "";
965 position: absolute;
966 top: 3px;
967 left: 3px;
968 width: 14px;
969 height: 14px;
970 border-radius: 50%;
971 background: var(--text-secondary);
972 transition: transform 0.2s, background 0.2s;
973}
974
975.settings-toggle.on {
976 background: var(--accent);
977}
978
979.settings-toggle.on::after {
980 transform: translateX(16px);
981 background: #fff;
982}
983
984/* ── Header Maximize Button ──────────────────────────────────── */
985.maximize-btn {
986 padding: 5px 8px;
987 background: transparent;
988 border: 1px solid var(--border);
989 border-radius: 2px;
990 color: var(--text-secondary);
991 cursor: pointer;
992 font-size: 13px;
993 font-family: inherit;
994 transition: background 0.15s, color 0.15s;
995}
996
997.maximize-btn:hover {
998 background: var(--bg-tertiary);
999 color: var(--text-primary);
1000}
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
@@ -58,6 +58,29 @@ export function ensureGitRepo(projectPath: string): void {
58} 58}
59 59
60// --------------------------------------------------------------------------- 60// ---------------------------------------------------------------------------
61// Current branch query
62// ---------------------------------------------------------------------------
63
64/**
65 * Returns the name of the currently checked-out branch,
66 * or null if git is unavailable or HEAD is detached.
67 */
68export function getCurrentBranch(projectPath: string): string | null {
69 try {
70 return (
71 execFileSync("git", ["branch", "--show-current"], {
72 cwd: projectPath,
73 stdio: "pipe",
74 })
75 .toString()
76 .trim() || null
77 );
78 } catch {
79 return null;
80 }
81}
82
83// ---------------------------------------------------------------------------
61// Branch creation 84// Branch creation
62// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
63 86
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 @@
1import { app, BrowserWindow, Menu } from "electron"; 1import { app, BrowserWindow, Menu, ipcMain } from "electron";
2import path from "node:path"; 2import path from "node:path";
3import { getDb, closeDb } from "./db"; 3import { getDb, closeDb } from "./db";
4import { registerIpcHandlers } from "./ipc/handlers"; 4import { registerIpcHandlers } from "./ipc/handlers";
@@ -23,6 +23,18 @@ function createWindow() {
23 23
24 registerIpcHandlers(mainWindow); 24 registerIpcHandlers(mainWindow);
25 25
26 // Maximize toggle — works identically on Linux and macOS
27 ipcMain.handle("window:toggleMaximize", () => {
28 if (mainWindow!.isMaximized()) mainWindow!.unmaximize();
29 else mainWindow!.maximize();
30 });
31
32 // Push state to renderer so the button glyph stays accurate.
33 // On macOS, clicking the green traffic light also fires these events,
34 // keeping our custom button in sync with the native control.
35 mainWindow.on("maximize", () => mainWindow!.webContents.send("window:maximized", true));
36 mainWindow.on("unmaximize", () => mainWindow!.webContents.send("window:maximized", false));
37
26 if (isDev) { 38 if (isDev) {
27 const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173"; 39 const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173";
28 mainWindow.loadURL(url).finally(() => { 40 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";
3import * as sessions from "../db/sessions"; 3import * as sessions from "../db/sessions";
4import * as claude from "../claude"; 4import * as claude from "../claude";
5import * as settingsDb from "../db/settings"; 5import * as settingsDb from "../db/settings";
6import { createSessionBranch, ensureGitIgnore } from "../git"; 6import { createSessionBranch, ensureGitIgnore, ensureGitRepo, getCurrentBranch } from "../git";
7import type { UserPermissionMode } from "../claude/phases"; 7import type { UserPermissionMode } from "../claude/phases";
8import { getDefaultSystemPromptTemplate } from "../claude/phases"; 8import { getDefaultSystemPromptTemplate } from "../claude/phases";
9 9
@@ -113,10 +113,27 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void {
113 if (nextPhase === "implement") { 113 if (nextPhase === "implement") {
114 const project = projects.getProject(session.project_id); 114 const project = projects.getProject(session.project_id);
115 if (project) { 115 if (project) {
116 const branchName = createSessionBranch(project.path, session.name, session.id); 116 const branchingSetting = settingsDb.getSetting("git.branchingEnabled");
117 if (branchName) { 117 const branchingEnabled = branchingSetting === "true"; // opt-in; default = off
118 sessions.updateSession(sessionId, { git_branch: branchName }); 118
119 git_branch = branchName; 119 // Always ensure repo + gitignore so commits work regardless of mode
120 try { ensureGitIgnore(project.path); } catch { /* non-fatal */ }
121 try { ensureGitRepo(project.path); } catch { /* non-fatal */ }
122
123 if (branchingEnabled) {
124 // createSessionBranch internally calls ensureGitIgnore/ensureGitRepo again
125 // (belt-and-suspenders), then checks out a new claude-flow/<slug>-<id> branch
126 const branchName = createSessionBranch(project.path, session.name, session.id);
127 if (branchName) {
128 sessions.updateSession(sessionId, { git_branch: branchName });
129 git_branch = branchName;
130 }
131 } else {
132 // No new branch — commit to whatever branch is currently checked out.
133 // Store the branch name so autoCommitTurn's boolean guard passes.
134 const currentBranch = getCurrentBranch(project.path) ?? "main";
135 sessions.updateSession(sessionId, { git_branch: currentBranch });
136 git_branch = currentBranch;
120 } 137 }
121 } 138 }
122 } 139 }
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 {
62 62
63 // Dialogs 63 // Dialogs
64 selectDirectory: () => Promise<string | null>; 64 selectDirectory: () => Promise<string | null>;
65
66 // Window
67 toggleMaximize: () => Promise<void>;
68 onWindowMaximized: (cb: (isMaximized: boolean) => void) => () => void;
65} 69}
66 70
67const api: ClaudeFlowAPI = { 71const api: ClaudeFlowAPI = {
@@ -127,6 +131,14 @@ const api: ClaudeFlowAPI = {
127 const result = await ipcRenderer.invoke("dialog:selectDirectory"); 131 const result = await ipcRenderer.invoke("dialog:selectDirectory");
128 return result; 132 return result;
129 }, 133 },
134
135 // Window
136 toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"),
137 onWindowMaximized: (cb) => {
138 const handler = (_: IpcRendererEvent, val: boolean) => cb(val);
139 ipcRenderer.on("window:maximized", handler);
140 return () => ipcRenderer.removeListener("window:maximized", handler);
141 },
130}; 142};
131 143
132contextBridge.exposeInMainWorld("api", api); 144contextBridge.exposeInMainWorld("api", api);