aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--renderer/src/App.tsx7
-rw-r--r--renderer/src/components/Header.tsx7
-rw-r--r--renderer/src/components/SettingsPage.tsx51
-rw-r--r--renderer/src/components/settings/SystemPromptsSettings.tsx142
-rw-r--r--renderer/src/styles/globals.css213
-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
11 files changed, 511 insertions, 3 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index 74b1f91..7d75196 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -3,6 +3,7 @@ import { Header } from "./components/Header";
3import { DocumentPane } from "./components/DocumentPane"; 3import { DocumentPane } from "./components/DocumentPane";
4import { ChatPane } from "./components/ChatPane"; 4import { ChatPane } from "./components/ChatPane";
5import { ActionBar } from "./components/ActionBar"; 5import { ActionBar } from "./components/ActionBar";
6import { SettingsPage } from "./components/SettingsPage";
6import type { Project, Session, Message, Phase, TokenUsage } from "./types"; 7import type { Project, Session, Message, Phase, TokenUsage } from "./types";
7import "./styles/globals.css"; 8import "./styles/globals.css";
8 9
@@ -56,6 +57,7 @@ export function App() {
56 outputTokens: 0, 57 outputTokens: 0,
57 }); 58 });
58 const [error, setError] = useState<string | null>(null); 59 const [error, setError] = useState<string | null>(null);
60 const [showSettings, setShowSettings] = useState(false);
59 61
60 const [theme, setTheme] = useState<Theme>( 62 const [theme, setTheme] = useState<Theme>(
61 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" 63 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark"
@@ -365,6 +367,7 @@ export function App() {
365 theme={theme} 367 theme={theme}
366 onToggleTheme={handleToggleTheme} 368 onToggleTheme={handleToggleTheme}
367 gitBranch={selectedSession?.git_branch ?? null} 369 gitBranch={selectedSession?.git_branch ?? null}
370 onOpenSettings={() => setShowSettings(true)}
368 /> 371 />
369 372
370 <div className="main-content"> 373 <div className="main-content">
@@ -413,6 +416,10 @@ export function App() {
413 }} 416 }}
414 disabled={!selectedSession} 417 disabled={!selectedSession}
415 /> 418 />
419
420 {showSettings && (
421 <SettingsPage onClose={() => setShowSettings(false)} />
422 )}
416 </div> 423 </div>
417 ); 424 );
418} 425}
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx
index fc0289d..3a530d3 100644
--- a/renderer/src/components/Header.tsx
+++ b/renderer/src/components/Header.tsx
@@ -18,6 +18,7 @@ interface HeaderProps {
18 theme: Theme; 18 theme: Theme;
19 onToggleTheme: () => void; 19 onToggleTheme: () => void;
20 gitBranch: string | null; 20 gitBranch: string | null;
21 onOpenSettings: () => void;
21} 22}
22 23
23const phaseLabels: Record<Phase, string> = { 24const phaseLabels: Record<Phase, string> = {
@@ -43,6 +44,7 @@ export function Header({
43 theme, 44 theme,
44 onToggleTheme, 45 onToggleTheme,
45 gitBranch, 46 gitBranch,
47 onOpenSettings,
46}: HeaderProps) { 48}: HeaderProps) {
47 const handleDeleteProject = () => { 49 const handleDeleteProject = () => {
48 if (!selectedProject || !onDeleteProject) return; 50 if (!selectedProject || !onDeleteProject) return;
@@ -222,6 +224,11 @@ export function Header({
222 <button className="theme-toggle" onClick={onToggleTheme}> 224 <button className="theme-toggle" onClick={onToggleTheme}>
223 {theme === "dark" ? "[light]" : "[dark]"} 225 {theme === "dark" ? "[light]" : "[dark]"}
224 </button> 226 </button>
227
228 {/* ── Settings button ── */}
229 <button className="settings-btn" onClick={onOpenSettings} title="Settings">
230 &#9881;
231 </button>
225 </div> 232 </div>
226 </header> 233 </header>
227 ); 234 );
diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx
new file mode 100644
index 0000000..5267665
--- /dev/null
+++ b/renderer/src/components/SettingsPage.tsx
@@ -0,0 +1,51 @@
1import React, { useState } from "react";
2import { SystemPromptsSettings } from "./settings/SystemPromptsSettings";
3
4type SettingsSection = "system-prompts";
5
6interface SettingsPageProps {
7 onClose: () => void;
8}
9
10export function SettingsPage({ onClose }: SettingsPageProps) {
11 const [activeSection, setActiveSection] =
12 useState<SettingsSection>("system-prompts");
13
14 return (
15 <div className="settings-overlay">
16 {/* Header — matches the main app header height/style */}
17 <div className="settings-header">
18 <div className="settings-header-left">
19 <span className="settings-title">\u2699 Settings</span>
20 </div>
21 <button
22 className="settings-close"
23 onClick={onClose}
24 title="Close settings"
25 >
26 \u00d7
27 </button>
28 </div>
29
30 <div className="settings-body">
31 {/* Side nav */}
32 <nav className="settings-nav">
33 <button
34 className={`settings-nav-item${
35 activeSection === "system-prompts" ? " active" : ""
36 }`}
37 onClick={() => setActiveSection("system-prompts")}
38 >
39 System Prompts
40 </button>
41 {/* Future sections added here */}
42 </nav>
43
44 {/* Content */}
45 <div className="settings-content">
46 {activeSection === "system-prompts" && <SystemPromptsSettings />}
47 </div>
48 </div>
49 </div>
50 );
51}
diff --git a/renderer/src/components/settings/SystemPromptsSettings.tsx b/renderer/src/components/settings/SystemPromptsSettings.tsx
new file mode 100644
index 0000000..a0c6a22
--- /dev/null
+++ b/renderer/src/components/settings/SystemPromptsSettings.tsx
@@ -0,0 +1,142 @@
1import React, { useState, useEffect } from "react";
2import type { Phase } from "../../types";
3
4const PHASES: Phase[] = ["research", "plan", "implement"];
5const PHASE_LABELS: Record<Phase, string> = {
6 research: "Research",
7 plan: "Plan",
8 implement: "Implement",
9};
10
11const api = window.api;
12
13export function SystemPromptsSettings() {
14 const [activePhase, setActivePhase] = useState<Phase>("research");
15 // Hardcoded default text per phase (fetched once from main process)
16 const [defaults, setDefaults] = useState<Record<Phase, string> | null>(null);
17 // What is currently saved in the DB per phase (null = not customized)
18 const [saved, setSaved] = useState<Record<Phase, string | null>>({
19 research: null,
20 plan: null,
21 implement: null,
22 });
23 // Live textarea content per phase — initialised to saved ?? default on load
24 const [texts, setTexts] = useState<Record<Phase, string>>({
25 research: "",
26 plan: "",
27 implement: "",
28 });
29 const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle");
30
31 useEffect(() => {
32 Promise.all([
33 api.getDefaultSystemPrompts(),
34 api.getSettings([
35 "systemPrompt.research",
36 "systemPrompt.plan",
37 "systemPrompt.implement",
38 ]),
39 ]).then(([defs, settings]) => {
40 const d = defs as Record<Phase, string>;
41 const s: Record<Phase, string | null> = {
42 research: settings["systemPrompt.research"],
43 plan: settings["systemPrompt.plan"],
44 implement: settings["systemPrompt.implement"],
45 };
46 setDefaults(d);
47 setSaved(s);
48 setTexts({
49 research: s.research ?? d.research,
50 plan: s.plan ?? d.plan,
51 implement: s.implement ?? d.implement,
52 });
53 });
54 }, []);
55
56 if (!defaults) {
57 return (
58 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
59 Loading...
60 </div>
61 );
62 }
63
64 const currentText = texts[activePhase];
65 const isCustomized = saved[activePhase] !== null;
66 // Dirty = textarea differs from what is in DB (or from default if not customized)
67 const isDirty = currentText !== (saved[activePhase] ?? defaults[activePhase]);
68
69 const handleChange = (val: string) => {
70 setTexts(prev => ({ ...prev, [activePhase]: val }));
71 setSaveStatus("idle");
72 };
73
74 const handleSave = async () => {
75 setSaveStatus("saving");
76 await api.setSetting(`systemPrompt.${activePhase}`, currentText);
77 setSaved(prev => ({ ...prev, [activePhase]: currentText }));
78 setSaveStatus("saved");
79 setTimeout(() => setSaveStatus("idle"), 2000);
80 };
81
82 const handleReset = async () => {
83 await api.deleteSetting(`systemPrompt.${activePhase}`);
84 setSaved(prev => ({ ...prev, [activePhase]: null }));
85 setTexts(prev => ({ ...prev, [activePhase]: defaults[activePhase] }));
86 setSaveStatus("idle");
87 };
88
89 return (
90 <div>
91 <div className="settings-section-title">System Prompts</div>
92 <div className="settings-section-desc">
93 Customize the instructions sent to Claude at the start of each workflow
94 phase. Changes take effect on the next message sent in any session.
95 </div>
96
97 <div className="settings-tabs">
98 {PHASES.map(phase => (
99 <button
100 key={phase}
101 className={`settings-tab${activePhase === phase ? " active" : ""}`}
102 onClick={() => setActivePhase(phase)}
103 >
104 {PHASE_LABELS[phase]}
105 {saved[phase] !== null ? " \u25cf" : ""}
106 </button>
107 ))}
108 </div>
109
110 <textarea
111 className={`settings-textarea${isCustomized ? " is-custom" : ""}`}
112 value={currentText}
113 onChange={e => handleChange(e.target.value)}
114 spellCheck={false}
115 />
116
117 <div className="settings-actions">
118 {isCustomized && (
119 <button className="btn-secondary" onClick={handleReset}>
120 Reset to Default
121 </button>
122 )}
123 <button
124 className="btn-primary"
125 onClick={handleSave}
126 disabled={saveStatus === "saving" || !isDirty}
127 >
128 {saveStatus === "saving"
129 ? "Saving\u2026"
130 : saveStatus === "saved"
131 ? "Saved \u2713"
132 : "Save"}
133 </button>
134 {(isCustomized || isDirty) && (
135 <span className="settings-custom-badge">
136 {isDirty ? "unsaved" : "custom"}
137 </span>
138 )}
139 </div>
140 </div>
141 );
142}
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index ef0275e..61a37c0 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -692,3 +692,216 @@ html[data-theme="light"] .chat-input input:focus {
692 border-left: 3px solid var(--accent); 692 border-left: 3px solid var(--accent);
693 border-radius: 0 2px 2px 0; 693 border-radius: 0 2px 2px 0;
694} 694}
695
696/* ── Settings Overlay ────────────────────────────────────────── */
697.settings-overlay {
698 position: fixed;
699 inset: 0;
700 background: var(--bg-primary);
701 z-index: 100;
702 display: flex;
703 flex-direction: column;
704}
705
706.settings-header {
707 display: flex;
708 justify-content: space-between;
709 align-items: center;
710 padding: 10px 16px;
711 background: var(--bg-secondary);
712 border-bottom: 1px solid var(--border);
713 -webkit-app-region: drag;
714}
715
716.settings-header-left {
717 display: flex;
718 align-items: center;
719 gap: 10px;
720 -webkit-app-region: no-drag;
721}
722
723.settings-title {
724 font-size: 12px;
725 font-weight: 700;
726 letter-spacing: 0.15em;
727 text-transform: uppercase;
728 color: var(--text-primary);
729}
730
731.settings-close {
732 background: transparent;
733 border: 1px solid var(--border);
734 border-radius: 2px;
735 color: var(--text-primary);
736 cursor: pointer;
737 font-size: 14px;
738 padding: 4px 9px;
739 font-family: inherit;
740 -webkit-app-region: no-drag;
741 transition: background 0.15s;
742}
743
744.settings-close:hover {
745 background: var(--bg-tertiary);
746}
747
748.settings-body {
749 flex: 1;
750 display: flex;
751 overflow: hidden;
752}
753
754/* ── Settings Side Nav ───────────────────────────────────────── */
755.settings-nav {
756 width: 180px;
757 background: var(--bg-secondary);
758 border-right: 1px solid var(--border);
759 padding: 16px 0;
760 flex-shrink: 0;
761}
762
763.settings-nav-item {
764 display: block;
765 width: 100%;
766 text-align: left;
767 padding: 8px 16px;
768 background: none;
769 border: none;
770 border-left: 2px solid transparent;
771 cursor: pointer;
772 font-family: inherit;
773 font-size: 12px;
774 letter-spacing: 0.05em;
775 color: var(--text-secondary);
776 transition: background 0.1s, color 0.1s;
777}
778
779.settings-nav-item:hover {
780 background: var(--bg-tertiary);
781 color: var(--text-primary);
782}
783
784.settings-nav-item.active {
785 color: var(--text-primary);
786 border-left-color: var(--accent);
787 background: var(--bg-tertiary);
788}
789
790/* ── Settings Content Area ───────────────────────────────────── */
791.settings-content {
792 flex: 1;
793 overflow-y: auto;
794 padding: 32px 40px;
795 max-width: 860px;
796}
797
798.settings-section-title {
799 font-size: 13px;
800 font-weight: 700;
801 letter-spacing: 0.1em;
802 text-transform: uppercase;
803 color: var(--text-primary);
804 margin-bottom: 6px;
805}
806
807.settings-section-desc {
808 font-size: 12px;
809 color: var(--text-secondary);
810 line-height: 1.6;
811 margin-bottom: 24px;
812}
813
814/* ── Phase Tab Strip ─────────────────────────────────────────── */
815.settings-tabs {
816 display: flex;
817 border-bottom: 1px solid var(--border);
818 margin-bottom: 16px;
819}
820
821.settings-tab {
822 padding: 6px 16px;
823 background: none;
824 border: none;
825 border-bottom: 2px solid transparent;
826 margin-bottom: -1px;
827 cursor: pointer;
828 font-family: inherit;
829 font-size: 11px;
830 letter-spacing: 0.07em;
831 text-transform: uppercase;
832 color: var(--text-secondary);
833 transition: color 0.1s;
834}
835
836.settings-tab:hover {
837 color: var(--text-primary);
838}
839
840.settings-tab.active {
841 color: var(--accent);
842 border-bottom-color: var(--accent);
843}
844
845/* ── Prompt Textarea ─────────────────────────────────────────── */
846.settings-textarea {
847 width: 100%;
848 min-height: 420px;
849 padding: 14px;
850 background: var(--bg-secondary);
851 border: 1px solid var(--border);
852 border-radius: 2px;
853 color: var(--text-primary);
854 font-family: inherit;
855 font-size: 12px;
856 line-height: 1.7;
857 resize: vertical;
858 transition: border-color 0.15s, box-shadow 0.15s;
859}
860
861.settings-textarea:focus {
862 outline: none;
863 border-color: var(--accent);
864 box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.15);
865}
866
867html[data-theme="light"] .settings-textarea:focus {
868 box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
869}
870
871.settings-textarea.is-custom {
872 border-color: var(--warning);
873}
874
875/* ── Settings Actions Row ────────────────────────────────────── */
876.settings-actions {
877 display: flex;
878 align-items: center;
879 gap: 10px;
880 margin-top: 12px;
881}
882
883.settings-custom-badge {
884 margin-left: auto;
885 font-size: 10px;
886 letter-spacing: 0.08em;
887 text-transform: uppercase;
888 color: var(--warning);
889}
890
891/* ── Header Settings Button ──────────────────────────────────── */
892.settings-btn {
893 padding: 5px 8px;
894 background: transparent;
895 border: 1px solid var(--border);
896 border-radius: 2px;
897 color: var(--text-secondary);
898 cursor: pointer;
899 font-size: 14px;
900 font-family: inherit;
901 transition: background 0.15s, color 0.15s;
902}
903
904.settings-btn:hover {
905 background: var(--bg-tertiary);
906 color: var(--text-primary);
907}
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");