From 04c63d4ef601876186e5d7fab980d76575c494ec Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 28 Feb 2026 21:08:40 -0800 Subject: feat: **1. `src/main/db/schema.ts`** — add `settings` table … (+10 more) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ **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 ``; pass `onOpenSettings` to Header --- src/main/claude/index.ts | 7 ++++++- src/main/claude/phases.ts | 22 ++++++++++++++++++++-- src/main/db/schema.ts | 5 +++++ src/main/db/settings.ts | 28 ++++++++++++++++++++++++++++ src/main/ipc/handlers.ts | 20 ++++++++++++++++++++ src/main/preload.ts | 12 ++++++++++++ 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/main/db/settings.ts (limited to 'src') 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"; import type { Phase, UserPermissionMode } from "./phases"; import { getProject } from "../db/projects"; import { updateSession } from "../db/sessions"; +import { getSetting } from "../db/settings"; import { autoCommitTurn } from "../git"; import fs from "node:fs"; import path from "node:path"; @@ -44,10 +45,14 @@ export async function sendMessage({ const sessionDir = getSessionDir(project.path, session.id); ensureDir(sessionDir); + // Load any custom system prompt for this phase (null → use default) + const customSystemPrompt = getSetting(`systemPrompt.${session.phase}`) ?? undefined; + const phaseConfig = getPhaseConfig( session.phase as Phase, sessionDir, - session.permission_mode as UserPermissionMode + session.permission_mode as UserPermissionMode, + customSystemPrompt ); 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.`, export function getPhaseConfig( phase: Phase, artifactDir: string, - userPermissionMode?: UserPermissionMode + userPermissionMode?: UserPermissionMode, + customSystemPrompt?: string ): PhaseConfig { const template = phaseConfigTemplates[phase]; + + // If a custom prompt is provided, substitute the {{artifactDir}} placeholder. + // Otherwise use the default template function (existing behaviour). + const systemPrompt = + customSystemPrompt !== undefined + ? customSystemPrompt.replace(/\{\{artifactDir\}\}/g, artifactDir) + : template.systemPrompt(artifactDir); + const config: PhaseConfig = { - systemPrompt: template.systemPrompt(artifactDir), + systemPrompt, tools: template.tools, permissionMode: template.permissionMode, initialMessage: template.initialMessage, @@ -175,6 +184,15 @@ export function getPhaseConfig( return config; } +/** + * Returns the default system prompt for a phase with "{{artifactDir}}" as a + * literal placeholder — the same format used when storing a custom prompt in + * the settings DB. Used by the Settings UI to display the default text. + */ +export function getDefaultSystemPromptTemplate(phase: Phase): string { + return phaseConfigTemplates[phase].systemPrompt("{{artifactDir}}"); +} + export function getPhaseInitialMessage(phase: Phase): string { return phaseConfigTemplates[phase].initialMessage; } 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) { created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); `); 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 @@ +import { getDb } from "./index"; + +export function getSetting(key: string): string | null { + const row = getDb() + .prepare("SELECT value FROM settings WHERE key = ?") + .get(key) as { value: string } | undefined; + return row ? row.value : null; +} + +export function getSettings(keys: string[]): Record { + const result: Record = {}; + for (const key of keys) { + result[key] = getSetting(key); + } + return result; +} + +export function setSetting(key: string, value: string): void { + getDb() + .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + .run(key, value); +} + +export function deleteSetting(key: string): void { + getDb() + .prepare("DELETE FROM settings WHERE key = ?") + .run(key); +} 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"; 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 type { UserPermissionMode } from "../claude/phases"; +import { getDefaultSystemPromptTemplate } from "../claude/phases"; export function registerIpcHandlers(mainWindow: BrowserWindow): void { // Projects @@ -155,6 +157,24 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void { claude.writeClaudeMd(projectPath, content); }); + // Settings + ipcMain.handle("settings:get", (_, keys: string[]) => + settingsDb.getSettings(keys) + ); + ipcMain.handle("settings:set", (_, key: string, value: string) => + settingsDb.setSetting(key, value) + ); + ipcMain.handle("settings:delete", (_, key: string) => + settingsDb.deleteSetting(key) + ); + + // Returns default prompt text (with {{artifactDir}} placeholder) for all phases + ipcMain.handle("settings:getDefaultPrompts", () => ({ + research: getDefaultSystemPromptTemplate("research"), + plan: getDefaultSystemPromptTemplate("plan"), + implement: getDefaultSystemPromptTemplate("implement"), + })); + // Dialogs ipcMain.handle("dialog:selectDirectory", async () => { 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 { callback: (sessionId: string, message: SDKMessage) => void ) => () => void; + // Settings + getSettings: (keys: string[]) => Promise>; + setSetting: (key: string, value: string) => Promise; + deleteSetting: (key: string) => Promise; + getDefaultSystemPrompts: () => Promise>; + // Dialogs selectDirectory: () => Promise; } @@ -110,6 +116,12 @@ const api: ClaudeFlowAPI = { return () => ipcRenderer.removeListener("claude:message", handler); }, + // Settings + getSettings: (keys) => ipcRenderer.invoke("settings:get", keys), + setSetting: (key, value) => ipcRenderer.invoke("settings:set", key, value), + deleteSetting: (key) => ipcRenderer.invoke("settings:delete", key), + getDefaultSystemPrompts: () => ipcRenderer.invoke("settings:getDefaultPrompts"), + // Dialogs selectDirectory: async () => { const result = await ipcRenderer.invoke("dialog:selectDirectory"); -- cgit v1.2.3