diff options
| author | bndw <ben@bdw.to> | 2026-02-28 21:08:40 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-28 21:08:40 -0800 |
| commit | 04c63d4ef601876186e5d7fab980d76575c494ec (patch) | |
| tree | 2620784e148957ae2ee3af0327c2b128983577e7 /renderer/src/components/settings | |
| parent | 0da42e4fa414ab3268d4f71896455097239f8590 (diff) | |
feat: **1. `src/main/db/schema.ts`** — add `settings` table … (+10 more)
- ✅ **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 `<SettingsPage>`; pass `onOpenSettings` to Header
Diffstat (limited to 'renderer/src/components/settings')
| -rw-r--r-- | renderer/src/components/settings/SystemPromptsSettings.tsx | 142 |
1 files changed, 142 insertions, 0 deletions
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 @@ | |||
| 1 | import React, { useState, useEffect } from "react"; | ||
| 2 | import type { Phase } from "../../types"; | ||
| 3 | |||
| 4 | const PHASES: Phase[] = ["research", "plan", "implement"]; | ||
| 5 | const PHASE_LABELS: Record<Phase, string> = { | ||
| 6 | research: "Research", | ||
| 7 | plan: "Plan", | ||
| 8 | implement: "Implement", | ||
| 9 | }; | ||
| 10 | |||
| 11 | const api = window.api; | ||
| 12 | |||
| 13 | export 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 | } | ||
