aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-28 21:08:40 -0800
committerbndw <ben@bdw.to>2026-02-28 21:08:40 -0800
commit04c63d4ef601876186e5d7fab980d76575c494ec (patch)
tree2620784e148957ae2ee3af0327c2b128983577e7 /src
parent0da42e4fa414ab3268d4f71896455097239f8590 (diff)
feat: **1. `src/main/db/schema.ts`** — add `settings` table … (+10 more)
- ✅ **1. `src/main/db/schema.ts`** — add `settings` table to `initSchema` - ✅ **2. `src/main/db/settings.ts`** — create file with `getSetting`, `getSettings`, `setSetting`, `deleteSetting` - ✅ **3. `src/main/claude/phases.ts`** — add `customSystemPrompt?` param to `getPhaseConfig`; add `getDefaultSystemPromptTemplate` export - ✅ **4. `src/main/claude/index.ts`** — import `getSetting`; load custom prompt in `sendMessage`; pass to `getPhaseConfig` - ✅ **5. `src/main/ipc/handlers.ts`** — import `settingsDb` + `getDefaultSystemPromptTemplate`; register `settings:get`, `settings:set`, `settings:delete`, `settings:getDefaultPrompts` - ✅ **6. `src/main/preload.ts`** — add `getSettings`, `setSetting`, `deleteSetting`, `getDefaultSystemPrompts` to interface + api object - ✅ **7. `renderer/src/styles/globals.css`** — append all new CSS rules - ✅ **8. `renderer/src/components/settings/SystemPromptsSettings.tsx`** — create file (new directory) - ✅ **9. `renderer/src/components/SettingsPage.tsx`** — create file - ✅ **10. `renderer/src/components/Header.tsx`** — add `onOpenSettings` prop + ⚙ button - ✅ **11. `renderer/src/App.tsx`** — add `showSettings` state; import + render `<SettingsPage>`; pass `onOpenSettings` to Header
Diffstat (limited to 'src')
-rw-r--r--src/main/claude/index.ts7
-rw-r--r--src/main/claude/phases.ts22
-rw-r--r--src/main/db/schema.ts5
-rw-r--r--src/main/db/settings.ts28
-rw-r--r--src/main/ipc/handlers.ts20
-rw-r--r--src/main/preload.ts12
6 files changed, 91 insertions, 3 deletions
diff --git a/src/main/claude/index.ts b/src/main/claude/index.ts
index 8971844..30a0f57 100644
--- a/src/main/claude/index.ts
+++ b/src/main/claude/index.ts
@@ -4,6 +4,7 @@ import { getPhaseConfig, getNextPhase, getArtifactFilename } from "./phases";
4import type { Phase, UserPermissionMode } from "./phases"; 4import type { Phase, UserPermissionMode } from "./phases";
5import { getProject } from "../db/projects"; 5import { getProject } from "../db/projects";
6import { updateSession } from "../db/sessions"; 6import { updateSession } from "../db/sessions";
7import { getSetting } from "../db/settings";
7import { autoCommitTurn } from "../git"; 8import { autoCommitTurn } from "../git";
8import fs from "node:fs"; 9import fs from "node:fs";
9import path from "node:path"; 10import path from "node:path";
@@ -44,10 +45,14 @@ export async function sendMessage({
44 const sessionDir = getSessionDir(project.path, session.id); 45 const sessionDir = getSessionDir(project.path, session.id);
45 ensureDir(sessionDir); 46 ensureDir(sessionDir);
46 47
48 // Load any custom system prompt for this phase (null → use default)
49 const customSystemPrompt = getSetting(`systemPrompt.${session.phase}`) ?? undefined;
50
47 const phaseConfig = getPhaseConfig( 51 const phaseConfig = getPhaseConfig(
48 session.phase as Phase, 52 session.phase as Phase,
49 sessionDir, 53 sessionDir,
50 session.permission_mode as UserPermissionMode 54 session.permission_mode as UserPermissionMode,
55 customSystemPrompt
51 ); 56 );
52 57
53 const q = query({ 58 const q = query({
diff --git a/src/main/claude/phases.ts b/src/main/claude/phases.ts
index 89e7c22..a1cbba1 100644
--- a/src/main/claude/phases.ts
+++ b/src/main/claude/phases.ts
@@ -160,11 +160,20 @@ When complete, summarize what was done and any follow-up tasks.`,
160export function getPhaseConfig( 160export function getPhaseConfig(
161 phase: Phase, 161 phase: Phase,
162 artifactDir: string, 162 artifactDir: string,
163 userPermissionMode?: UserPermissionMode 163 userPermissionMode?: UserPermissionMode,
164 customSystemPrompt?: string
164): PhaseConfig { 165): PhaseConfig {
165 const template = phaseConfigTemplates[phase]; 166 const template = phaseConfigTemplates[phase];
167
168 // If a custom prompt is provided, substitute the {{artifactDir}} placeholder.
169 // Otherwise use the default template function (existing behaviour).
170 const systemPrompt =
171 customSystemPrompt !== undefined
172 ? customSystemPrompt.replace(/\{\{artifactDir\}\}/g, artifactDir)
173 : template.systemPrompt(artifactDir);
174
166 const config: PhaseConfig = { 175 const config: PhaseConfig = {
167 systemPrompt: template.systemPrompt(artifactDir), 176 systemPrompt,
168 tools: template.tools, 177 tools: template.tools,
169 permissionMode: template.permissionMode, 178 permissionMode: template.permissionMode,
170 initialMessage: template.initialMessage, 179 initialMessage: template.initialMessage,
@@ -175,6 +184,15 @@ export function getPhaseConfig(
175 return config; 184 return config;
176} 185}
177 186
187/**
188 * Returns the default system prompt for a phase with "{{artifactDir}}" as a
189 * literal placeholder — the same format used when storing a custom prompt in
190 * the settings DB. Used by the Settings UI to display the default text.
191 */
192export function getDefaultSystemPromptTemplate(phase: Phase): string {
193 return phaseConfigTemplates[phase].systemPrompt("{{artifactDir}}");
194}
195
178export function getPhaseInitialMessage(phase: Phase): string { 196export function getPhaseInitialMessage(phase: Phase): string {
179 return phaseConfigTemplates[phase].initialMessage; 197 return phaseConfigTemplates[phase].initialMessage;
180} 198}
diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts
index 39ee567..4c24c6a 100644
--- a/src/main/db/schema.ts
+++ b/src/main/db/schema.ts
@@ -30,6 +30,11 @@ export function initSchema(db: Database.Database) {
30 created_at INTEGER NOT NULL DEFAULT (unixepoch()) 30 created_at INTEGER NOT NULL DEFAULT (unixepoch())
31 ); 31 );
32 32
33 CREATE TABLE IF NOT EXISTS settings (
34 key TEXT PRIMARY KEY,
35 value TEXT NOT NULL
36 );
37
33 CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); 38 CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
34 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); 39 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
35 `); 40 `);
diff --git a/src/main/db/settings.ts b/src/main/db/settings.ts
new file mode 100644
index 0000000..1f86a9a
--- /dev/null
+++ b/src/main/db/settings.ts
@@ -0,0 +1,28 @@
1import { getDb } from "./index";
2
3export function getSetting(key: string): string | null {
4 const row = getDb()
5 .prepare("SELECT value FROM settings WHERE key = ?")
6 .get(key) as { value: string } | undefined;
7 return row ? row.value : null;
8}
9
10export function getSettings(keys: string[]): Record<string, string | null> {
11 const result: Record<string, string | null> = {};
12 for (const key of keys) {
13 result[key] = getSetting(key);
14 }
15 return result;
16}
17
18export function setSetting(key: string, value: string): void {
19 getDb()
20 .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
21 .run(key, value);
22}
23
24export function deleteSetting(key: string): void {
25 getDb()
26 .prepare("DELETE FROM settings WHERE key = ?")
27 .run(key);
28}
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts
index 774eb63..bc7d78d 100644
--- a/src/main/ipc/handlers.ts
+++ b/src/main/ipc/handlers.ts
@@ -2,8 +2,10 @@ 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 * as settingsDb from "../db/settings";
5import { createSessionBranch, ensureGitIgnore } from "../git"; 6import { createSessionBranch, ensureGitIgnore } from "../git";
6import type { UserPermissionMode } from "../claude/phases"; 7import type { UserPermissionMode } from "../claude/phases";
8import { getDefaultSystemPromptTemplate } from "../claude/phases";
7 9
8export function registerIpcHandlers(mainWindow: BrowserWindow): void { 10export function registerIpcHandlers(mainWindow: BrowserWindow): void {
9 // Projects 11 // Projects
@@ -155,6 +157,24 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void {
155 claude.writeClaudeMd(projectPath, content); 157 claude.writeClaudeMd(projectPath, content);
156 }); 158 });
157 159
160 // Settings
161 ipcMain.handle("settings:get", (_, keys: string[]) =>
162 settingsDb.getSettings(keys)
163 );
164 ipcMain.handle("settings:set", (_, key: string, value: string) =>
165 settingsDb.setSetting(key, value)
166 );
167 ipcMain.handle("settings:delete", (_, key: string) =>
168 settingsDb.deleteSetting(key)
169 );
170
171 // Returns default prompt text (with {{artifactDir}} placeholder) for all phases
172 ipcMain.handle("settings:getDefaultPrompts", () => ({
173 research: getDefaultSystemPromptTemplate("research"),
174 plan: getDefaultSystemPromptTemplate("plan"),
175 implement: getDefaultSystemPromptTemplate("implement"),
176 }));
177
158 // Dialogs 178 // Dialogs
159 ipcMain.handle("dialog:selectDirectory", async () => { 179 ipcMain.handle("dialog:selectDirectory", async () => {
160 const result = await dialog.showOpenDialog(mainWindow, { 180 const result = await dialog.showOpenDialog(mainWindow, {
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 299a1b5..52e947b 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -54,6 +54,12 @@ export interface ClaudeFlowAPI {
54 callback: (sessionId: string, message: SDKMessage) => void 54 callback: (sessionId: string, message: SDKMessage) => void
55 ) => () => void; 55 ) => () => void;
56 56
57 // Settings
58 getSettings: (keys: string[]) => Promise<Record<string, string | null>>;
59 setSetting: (key: string, value: string) => Promise<void>;
60 deleteSetting: (key: string) => Promise<void>;
61 getDefaultSystemPrompts: () => Promise<Record<"research" | "plan" | "implement", string>>;
62
57 // Dialogs 63 // Dialogs
58 selectDirectory: () => Promise<string | null>; 64 selectDirectory: () => Promise<string | null>;
59} 65}
@@ -110,6 +116,12 @@ const api: ClaudeFlowAPI = {
110 return () => ipcRenderer.removeListener("claude:message", handler); 116 return () => ipcRenderer.removeListener("claude:message", handler);
111 }, 117 },
112 118
119 // Settings
120 getSettings: (keys) => ipcRenderer.invoke("settings:get", keys),
121 setSetting: (key, value) => ipcRenderer.invoke("settings:set", key, value),
122 deleteSetting: (key) => ipcRenderer.invoke("settings:delete", key),
123 getDefaultSystemPrompts: () => ipcRenderer.invoke("settings:getDefaultPrompts"),
124
113 // Dialogs 125 // Dialogs
114 selectDirectory: async () => { 126 selectDirectory: async () => {
115 const result = await ipcRenderer.invoke("dialog:selectDirectory"); 127 const result = await ipcRenderer.invoke("dialog:selectDirectory");