From 04c63d4ef601876186e5d7fab980d76575c494ec Mon Sep 17 00:00:00 2001
From: bndw <ben@bdw.to>
Date: Sat, 28 Feb 2026 21:08:40 -0800
Subject: feat: **1. `src/main/db/schema.ts`** — add `settings` table … (+10
 more)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- ✅ **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
---
 .../components/settings/SystemPromptsSettings.tsx  | 142 +++++++++++++++++++++
 1 file changed, 142 insertions(+)
 create mode 100644 renderer/src/components/settings/SystemPromptsSettings.tsx

(limited to 'renderer/src/components/settings/SystemPromptsSettings.tsx')

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 @@
+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>
+  );
+}
-- 
cgit v1.2.3

