aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-04 21:36:32 -0800
committerbndw <ben@bdw.to>2026-03-04 21:36:32 -0800
commit73d2680b83ccbdbd8dfec2d319533e98b379b830 (patch)
tree193eaf5157edcf12d06dde85fb44efaf3aae2004 /renderer/src/components
parentb6405dd6a4ba65fc5dc6746db7be7be7d0bb29f3 (diff)
feat: Thread optional `phase` param into `db/sessions.ts::cre… (+7 more)
- ✅ Thread optional `phase` param into `db/sessions.ts::createSession()` - ✅ Thread optional `phase` param into `ipc/handlers.ts` sessions:create handler - ✅ Thread optional `phase` param into `preload.ts` createSession API - ✅ Update Plan phase system prompt to gracefully handle missing research.md - ✅ Update Implement phase system prompt to gracefully handle missing plan.md - ✅ Create `renderer/src/components/NewSessionModal.tsx` - ✅ Update `App.tsx`: add modal state, split handler, add modal JSX - ✅ Add modal CSS to `globals.css`
Diffstat (limited to 'renderer/src/components')
-rw-r--r--renderer/src/components/DocumentPane.tsx15
-rw-r--r--renderer/src/components/NewSessionModal.tsx60
-rw-r--r--renderer/src/components/settings/ModelSettings.tsx156
3 files changed, 214 insertions, 17 deletions
diff --git a/renderer/src/components/DocumentPane.tsx b/renderer/src/components/DocumentPane.tsx
index f5368b3..2ec66f6 100644
--- a/renderer/src/components/DocumentPane.tsx
+++ b/renderer/src/components/DocumentPane.tsx
@@ -117,6 +117,11 @@ export function DocumentPane({
117}: DocumentPaneProps) { 117}: DocumentPaneProps) {
118 const [isEditing, setIsEditing] = useState(false); 118 const [isEditing, setIsEditing] = useState(false);
119 119
120 // Always exit edit mode when the pane becomes read-only.
121 useEffect(() => {
122 if (disabled) setIsEditing(false);
123 }, [disabled]);
124
120 if (showOnboarding) { 125 if (showOnboarding) {
121 return ( 126 return (
122 <div className="document-pane"> 127 <div className="document-pane">
@@ -206,9 +211,13 @@ export function DocumentPane({
206 <div className="document-pane"> 211 <div className="document-pane">
207 <div className="document-header"> 212 <div className="document-header">
208 <span>{filename}</span> 213 <span>{filename}</span>
209 <button onClick={() => setIsEditing(!isEditing)}> 214 {disabled ? (
210 {isEditing ? "Preview" : "Edit"} 215 <span className="badge badge-readonly">Read-only</span>
211 </button> 216 ) : (
217 <button onClick={() => setIsEditing(!isEditing)}>
218 {isEditing ? "Preview" : "Edit"}
219 </button>
220 )}
212 </div> 221 </div>
213 222
214 {isEditing ? ( 223 {isEditing ? (
diff --git a/renderer/src/components/NewSessionModal.tsx b/renderer/src/components/NewSessionModal.tsx
new file mode 100644
index 0000000..ad5dae9
--- /dev/null
+++ b/renderer/src/components/NewSessionModal.tsx
@@ -0,0 +1,60 @@
1import React, { useState, useEffect } from "react";
2import type { Phase } from "../types";
3
4interface NewSessionModalProps {
5 onConfirm: (phase: Phase) => void;
6 onCancel: () => void;
7}
8
9const phaseOptions: { phase: Phase; label: string; description: string }[] = [
10 { phase: "research", label: "Research", description: "Understand the codebase and requirements" },
11 { phase: "plan", label: "Plan", description: "Create a detailed implementation strategy" },
12 { phase: "implement", label: "Implement", description: "Execute the implementation plan" },
13];
14
15export function NewSessionModal({ onConfirm, onCancel }: NewSessionModalProps) {
16 const [selected, setSelected] = useState<Phase>("research");
17
18 // Close on Escape
19 useEffect(() => {
20 const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
21 window.addEventListener("keydown", onKey);
22 return () => window.removeEventListener("keydown", onKey);
23 }, [onCancel]);
24
25 return (
26 <div className="modal-backdrop" onClick={onCancel}>
27 <div className="modal" onClick={(e) => e.stopPropagation()}>
28
29 <div className="modal-header">
30 <span className="modal-title">New Session</span>
31 <button className="modal-close" onClick={onCancel}>×</button>
32 </div>
33
34 <div className="modal-body">
35 <p className="modal-prompt">Choose where to start</p>
36 <div className="phase-cards">
37 {phaseOptions.map(({ phase, label, description }) => (
38 <button
39 key={phase}
40 className={`phase-card${selected === phase ? " selected" : ""}`}
41 onClick={() => setSelected(phase)}
42 >
43 <span className="phase-card-label">{label}</span>
44 <span className="phase-card-desc">{description}</span>
45 </button>
46 ))}
47 </div>
48 </div>
49
50 <div className="modal-footer">
51 <button className="modal-btn-secondary" onClick={onCancel}>Cancel</button>
52 <button className="modal-btn-primary" onClick={() => onConfirm(selected)}>
53 Create
54 </button>
55 </div>
56
57 </div>
58 </div>
59 );
60}
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 );