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