1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
import React, { useState, useEffect } from "react";
import type { Phase } from "../../types";
const PHASES: Phase[] = ["research", "plan", "implement"];
const PHASE_LABELS: Record<Phase, string> = {
research: "Research",
plan: "Plan",
implement: "Implement",
};
const api = window.api;
export function SystemPromptsSettings() {
const [activePhase, setActivePhase] = useState<Phase>("research");
// Hardcoded default text per phase (fetched once from main process)
const [defaults, setDefaults] = useState<Record<Phase, string> | null>(null);
// What is currently saved in the DB per phase (null = not customized)
const [saved, setSaved] = useState<Record<Phase, string | null>>({
research: null,
plan: null,
implement: null,
});
// Live textarea content per phase — initialised to saved ?? default on load
const [texts, setTexts] = useState<Record<Phase, string>>({
research: "",
plan: "",
implement: "",
});
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle");
useEffect(() => {
Promise.all([
api.getDefaultSystemPrompts(),
api.getSettings([
"systemPrompt.research",
"systemPrompt.plan",
"systemPrompt.implement",
]),
]).then(([defs, settings]) => {
const d = defs as Record<Phase, string>;
const s: Record<Phase, string | null> = {
research: settings["systemPrompt.research"],
plan: settings["systemPrompt.plan"],
implement: settings["systemPrompt.implement"],
};
setDefaults(d);
setSaved(s);
setTexts({
research: s.research ?? d.research,
plan: s.plan ?? d.plan,
implement: s.implement ?? d.implement,
});
});
}, []);
if (!defaults) {
return (
<div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
Loading...
</div>
);
}
const currentText = texts[activePhase];
const isCustomized = saved[activePhase] !== null;
// Dirty = textarea differs from what is in DB (or from default if not customized)
const isDirty = currentText !== (saved[activePhase] ?? defaults[activePhase]);
const handleChange = (val: string) => {
setTexts(prev => ({ ...prev, [activePhase]: val }));
setSaveStatus("idle");
};
const handleSave = async () => {
setSaveStatus("saving");
await api.setSetting(`systemPrompt.${activePhase}`, currentText);
setSaved(prev => ({ ...prev, [activePhase]: currentText }));
setSaveStatus("saved");
setTimeout(() => setSaveStatus("idle"), 2000);
};
const handleReset = async () => {
await api.deleteSetting(`systemPrompt.${activePhase}`);
setSaved(prev => ({ ...prev, [activePhase]: null }));
setTexts(prev => ({ ...prev, [activePhase]: defaults[activePhase] }));
setSaveStatus("idle");
};
return (
<div>
<div className="settings-section-title">System Prompts</div>
<div className="settings-section-desc">
Customize the instructions sent to Claude at the start of each workflow
phase. Changes take effect on the next message sent in any session.
</div>
<div className="settings-tabs">
{PHASES.map(phase => (
<button
key={phase}
className={`settings-tab${activePhase === phase ? " active" : ""}`}
onClick={() => setActivePhase(phase)}
>
{PHASE_LABELS[phase]}
{saved[phase] !== null ? " \u25cf" : ""}
</button>
))}
</div>
<textarea
className={`settings-textarea${isCustomized ? " is-custom" : ""}`}
value={currentText}
onChange={e => handleChange(e.target.value)}
spellCheck={false}
/>
<div className="settings-actions">
{isCustomized && (
<button className="btn-secondary" onClick={handleReset}>
Reset to Default
</button>
)}
<button
className="btn-primary"
onClick={handleSave}
disabled={saveStatus === "saving" || !isDirty}
>
{saveStatus === "saving"
? "Saving\u2026"
: saveStatus === "saved"
? "Saved \u2713"
: "Save"}
</button>
{(isCustomized || isDirty) && (
<span className="settings-custom-badge">
{isDirty ? "unsaved" : "custom"}
</span>
)}
</div>
</div>
);
}
|