aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components/settings/ModelSettings.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/components/settings/ModelSettings.tsx')
-rw-r--r--renderer/src/components/settings/ModelSettings.tsx156
1 files changed, 142 insertions, 14 deletions
diff --git a/renderer/src/components/settings/ModelSettings.tsx b/renderer/src/components/settings/ModelSettings.tsx
index ecfc12c..f0cbda5 100644
--- a/renderer/src/components/settings/ModelSettings.tsx
+++ b/renderer/src/components/settings/ModelSettings.tsx
@@ -1,21 +1,61 @@
1import React, { useState, useEffect } from "react"; 1import React, { useState, useEffect } from "react";
2import type { Phase } from "../../types";
2 3
3const api = window.api; 4const api = window.api;
4 5
6const PHASES: Phase[] = ["research", "plan", "implement"];
7const PHASE_LABELS: Record<Phase, string> = {
8 research: "Research",
9 plan: "Plan",
10 implement: "Implement",
11};
12
5export function ModelSettings() { 13export function ModelSettings() {
6 // null = not yet loaded from DB 14 // ── Global default ───────────────────────────────────────────
7 const [model, setModel] = useState<string | null>(null); 15 const [model, setModel] = useState<string | null>(null);
8 const [draft, setDraft] = useState(""); 16 const [draft, setDraft] = useState("");
9 const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle"); 17 const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
10 18
19 // ── Per-phase overrides ──────────────────────────────────────
20 const [phaseModels, setPhaseModels] = useState<Record<Phase, string | null>>({
21 research: null,
22 plan: null,
23 implement: null,
24 });
25 const [phaseDrafts, setPhaseDrafts] = useState<Record<Phase, string>>({
26 research: "",
27 plan: "",
28 implement: "",
29 });
30 const [phaseStatus, setPhaseStatus] = useState<Record<Phase, "idle" | "saved">>({
31 research: "idle",
32 plan: "idle",
33 implement: "idle",
34 });
35
11 useEffect(() => { 36 useEffect(() => {
12 api.getSettings(["model"]).then((settings) => { 37 api
13 const saved = settings["model"] ?? ""; 38 .getSettings(["model", "model.research", "model.plan", "model.implement"])
14 setModel(saved); 39 .then((settings) => {
15 setDraft(saved); 40 const saved = settings["model"] ?? "";
16 }); 41 setModel(saved);
42 setDraft(saved);
43
44 const pm: Record<Phase, string | null> = {
45 research: settings["model.research"],
46 plan: settings["model.plan"],
47 implement: settings["model.implement"],
48 };
49 setPhaseModels(pm);
50 setPhaseDrafts({
51 research: pm.research ?? "",
52 plan: pm.plan ?? "",
53 implement: pm.implement ?? "",
54 });
55 });
17 }, []); 56 }, []);
18 57
58 // ── Global handlers ──────────────────────────────────────────
19 const handleSave = async () => { 59 const handleSave = async () => {
20 const trimmed = draft.trim(); 60 const trimmed = draft.trim();
21 if (trimmed) { 61 if (trimmed) {
@@ -37,6 +77,34 @@ export function ModelSettings() {
37 setTimeout(() => setSaveStatus("idle"), 1500); 77 setTimeout(() => setSaveStatus("idle"), 1500);
38 }; 78 };
39 79
80 // ── Per-phase handlers ───────────────────────────────────────
81 const handlePhaseSave = async (phase: Phase) => {
82 const trimmed = phaseDrafts[phase].trim();
83 if (trimmed) {
84 await api.setSetting(`model.${phase}`, trimmed);
85 setPhaseModels((prev) => ({ ...prev, [phase]: trimmed }));
86 } else {
87 await api.deleteSetting(`model.${phase}`);
88 setPhaseModels((prev) => ({ ...prev, [phase]: null }));
89 }
90 setPhaseStatus((prev) => ({ ...prev, [phase]: "saved" }));
91 setTimeout(
92 () => setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" })),
93 1500
94 );
95 };
96
97 const handlePhaseReset = async (phase: Phase) => {
98 await api.deleteSetting(`model.${phase}`);
99 setPhaseModels((prev) => ({ ...prev, [phase]: null }));
100 setPhaseDrafts((prev) => ({ ...prev, [phase]: "" }));
101 setPhaseStatus((prev) => ({ ...prev, [phase]: "saved" }));
102 setTimeout(
103 () => setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" })),
104 1500
105 );
106 };
107
40 if (model === null) { 108 if (model === null) {
41 return ( 109 return (
42 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}> 110 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
@@ -49,10 +117,11 @@ export function ModelSettings() {
49 117
50 return ( 118 return (
51 <div> 119 <div>
52 <div className="settings-section-title">Model</div> 120 {/* ── Global default ──────────────────────────────────── */}
121 <div className="settings-section-title">Default Model</div>
53 <div className="settings-section-desc"> 122 <div className="settings-section-desc">
54 Claude model to use for all phases. Leave blank to use the SDK&apos;s 123 Fallback model used for all phases unless a per-phase override is set.
55 default model. Takes effect on the next message sent in any session. 124 Leave blank to use the SDK&apos;s built-in default.
56 </div> 125 </div>
57 126
58 <div className="settings-toggle-row"> 127 <div className="settings-toggle-row">
@@ -60,7 +129,7 @@ export function ModelSettings() {
60 className="settings-model-input" 129 className="settings-model-input"
61 type="text" 130 type="text"
62 value={draft} 131 value={draft}
63 placeholder="Default (e.g. claude-sonnet-4-5)" 132 placeholder="e.g. claude-sonnet-4-5"
64 onChange={(e) => { 133 onChange={(e) => {
65 setDraft(e.target.value); 134 setDraft(e.target.value);
66 setSaveStatus("idle"); 135 setSaveStatus("idle");
@@ -75,7 +144,7 @@ export function ModelSettings() {
75 <div className="settings-actions"> 144 <div className="settings-actions">
76 {model && ( 145 {model && (
77 <button className="btn-secondary" onClick={handleReset}> 146 <button className="btn-secondary" onClick={handleReset}>
78 Reset to Default 147 Reset
79 </button> 148 </button>
80 )} 149 )}
81 <button 150 <button
@@ -85,9 +154,68 @@ export function ModelSettings() {
85 > 154 >
86 {saveStatus === "saved" ? "Saved \u2713" : "Save"} 155 {saveStatus === "saved" ? "Saved \u2713" : "Save"}
87 </button> 156 </button>
88 {model && ( 157 {model && <span className="settings-custom-badge">custom</span>}
89 <span className="settings-custom-badge">custom</span> 158 </div>
90 )} 159
160 {/* ── Per-phase overrides ─────────────────────────────── */}
161 <div className="settings-section-title" style={{ marginTop: 24 }}>
162 Phase Overrides
163 </div>
164 <div className="settings-section-desc">
165 Use a different model for a specific phase. Leave blank to inherit the
166 default above. Takes effect on the next message in any session.
167 </div>
168
169 <div className="settings-phase-model-list">
170 {PHASES.map((phase) => {
171 const saved = phaseModels[phase];
172 const phaseDirty = phaseDrafts[phase].trim() !== (saved ?? "");
173
174 return (
175 <div key={phase} className="settings-phase-model-row">
176 <span className="settings-phase-model-label">
177 {PHASE_LABELS[phase]}
178 </span>
179 <input
180 className="settings-model-input"
181 type="text"
182 value={phaseDrafts[phase]}
183 placeholder={model ? `Inherits: ${model}` : "Inherits default"}
184 onChange={(e) => {
185 setPhaseDrafts((prev) => ({
186 ...prev,
187 [phase]: e.target.value,
188 }));
189 setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" }));
190 }}
191 onKeyDown={(e) => {
192 if (e.key === "Enter") handlePhaseSave(phase);
193 }}
194 spellCheck={false}
195 />
196 <div className="settings-phase-model-actions">
197 {saved && (
198 <button
199 className="btn-secondary"
200 onClick={() => handlePhaseReset(phase)}
201 >
202 Reset
203 </button>
204 )}
205 <button
206 className="btn-primary"
207 onClick={() => handlePhaseSave(phase)}
208 disabled={!phaseDirty}
209 >
210 {phaseStatus[phase] === "saved" ? "Saved \u2713" : "Save"}
211 </button>
212 {saved && (
213 <span className="settings-custom-badge">custom</span>
214 )}
215 </div>
216 </div>
217 );
218 })}
91 </div> 219 </div>
92 </div> 220 </div>
93 ); 221 );