aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/components')
-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
3 files changed, 200 insertions, 0 deletions
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}