aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src
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
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')
-rw-r--r--renderer/src/App.tsx30
-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
-rw-r--r--renderer/src/styles/globals.css160
5 files changed, 402 insertions, 19 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index 719faac..7c5c969 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -5,6 +5,7 @@ import { DocumentPane } from "./components/DocumentPane";
5import { ChatPane } from "./components/ChatPane"; 5import { ChatPane } from "./components/ChatPane";
6import { ActionBar } from "./components/ActionBar"; 6import { ActionBar } from "./components/ActionBar";
7import { SettingsPage } from "./components/SettingsPage"; 7import { SettingsPage } from "./components/SettingsPage";
8import { NewSessionModal } from "./components/NewSessionModal";
8import type { Project, Session, Message, Phase, TokenUsage } from "./types"; 9import type { Project, Session, Message, Phase, TokenUsage } from "./types";
9import "./styles/globals.css"; 10import "./styles/globals.css";
10 11
@@ -66,6 +67,8 @@ export function App() {
66 }); 67 });
67 const [error, setError] = useState<string | null>(null); 68 const [error, setError] = useState<string | null>(null);
68 const [showSettings, setShowSettings] = useState(false); 69 const [showSettings, setShowSettings] = useState(false);
70 const [showNewSessionModal, setShowNewSessionModal] = useState(false);
71 const [newSessionProjectId, setNewSessionProjectId] = useState<string | null>(null);
69 const [activeModel, setActiveModel] = useState<string | null>(null); 72 const [activeModel, setActiveModel] = useState<string | null>(null);
70 73
71 const [theme, setTheme] = useState<Theme>( 74 const [theme, setTheme] = useState<Theme>(
@@ -451,12 +454,25 @@ export function App() {
451 setSelectedProject(project); 454 setSelectedProject(project);
452 }; 455 };
453 456
454 const handleCreateSession = async (projectId: string) => { 457 // Called by Sidebar when user clicks "+" — opens the phase-selection modal
458 const handleCreateSession = (projectId: string) => {
459 setNewSessionProjectId(projectId);
460 setShowNewSessionModal(true);
461 };
462
463 // Called by NewSessionModal when user clicks "Create"
464 const handleConfirmNewSession = async (phase: Phase) => {
465 setShowNewSessionModal(false);
466 const projectId = newSessionProjectId;
467 setNewSessionProjectId(null);
468 if (!projectId) return;
469
455 const project = projects.find((p) => p.id === projectId); 470 const project = projects.find((p) => p.id === projectId);
456 if (!project) return; 471 if (!project) return;
472
457 const projectSessions = sessions.filter((s) => s.project_id === projectId); 473 const projectSessions = sessions.filter((s) => s.project_id === projectId);
458 const name = `Session ${projectSessions.length + 1}`; 474 const name = `Session ${projectSessions.length + 1}`;
459 const session = await api.createSession(projectId, name); 475 const session = await api.createSession(projectId, name, phase);
460 setSessions((prev) => [session, ...prev]); 476 setSessions((prev) => [session, ...prev]);
461 setSelectedProject(project); 477 setSelectedProject(project);
462 setSelectedSession(session); 478 setSelectedSession(session);
@@ -623,6 +639,16 @@ export function App() {
623 {showSettings && ( 639 {showSettings && (
624 <SettingsPage onClose={() => setShowSettings(false)} /> 640 <SettingsPage onClose={() => setShowSettings(false)} />
625 )} 641 )}
642
643 {showNewSessionModal && (
644 <NewSessionModal
645 onConfirm={handleConfirmNewSession}
646 onCancel={() => {
647 setShowNewSessionModal(false);
648 setNewSessionProjectId(null);
649 }}
650 />
651 )}
626 </div> 652 </div>
627 ); 653 );
628} 654}
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 );
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index c463432..dc7d4a5 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -1711,6 +1711,166 @@ html[data-theme="light"] .settings-textarea:focus {
1711 margin-left: 4px; 1711 margin-left: 4px;
1712} 1712}
1713 1713
1714/* ── New Session Modal ───────────────────────────────────────── */
1715.modal-backdrop {
1716 position: fixed;
1717 inset: 0;
1718 background: rgba(0, 0, 0, 0.55);
1719 display: flex;
1720 align-items: center;
1721 justify-content: center;
1722 z-index: 200;
1723}
1724
1725.modal {
1726 background: var(--bg-secondary);
1727 border: 1px solid var(--border);
1728 border-radius: 4px;
1729 width: 400px;
1730 display: flex;
1731 flex-direction: column;
1732 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
1733}
1734
1735.modal-header {
1736 display: flex;
1737 justify-content: space-between;
1738 align-items: center;
1739 padding: 12px 16px;
1740 border-bottom: 1px solid var(--border);
1741}
1742
1743.modal-title {
1744 font-size: 13px;
1745 font-weight: 600;
1746 color: var(--text-primary);
1747 text-transform: uppercase;
1748 letter-spacing: 0.08em;
1749}
1750
1751.modal-close {
1752 background: transparent;
1753 border: 1px solid var(--border);
1754 border-radius: 2px;
1755 color: var(--text-secondary);
1756 font-size: 14px;
1757 width: 22px;
1758 height: 22px;
1759 cursor: pointer;
1760 display: flex;
1761 align-items: center;
1762 justify-content: center;
1763 transition: background 0.15s;
1764}
1765
1766.modal-close:hover {
1767 background: var(--bg-tertiary);
1768 color: var(--text-primary);
1769}
1770
1771.modal-body {
1772 padding: 20px 16px 16px;
1773 display: flex;
1774 flex-direction: column;
1775 gap: 12px;
1776}
1777
1778.modal-prompt {
1779 font-size: 12px;
1780 color: var(--text-secondary);
1781 text-transform: uppercase;
1782 letter-spacing: 0.08em;
1783}
1784
1785.phase-cards {
1786 display: flex;
1787 flex-direction: column;
1788 gap: 6px;
1789}
1790
1791.phase-card {
1792 display: flex;
1793 flex-direction: column;
1794 align-items: flex-start;
1795 gap: 2px;
1796 padding: 10px 12px;
1797 background: var(--bg-primary);
1798 border: 1px solid var(--border);
1799 border-radius: 3px;
1800 cursor: pointer;
1801 text-align: left;
1802 transition: border-color 0.15s, background 0.15s;
1803 width: 100%;
1804}
1805
1806.phase-card:hover {
1807 border-color: var(--accent);
1808 background: var(--bg-tertiary);
1809}
1810
1811.phase-card.selected {
1812 border-color: var(--accent);
1813 background: var(--bg-tertiary);
1814}
1815
1816.phase-card-label {
1817 font-size: 12px;
1818 font-weight: 600;
1819 color: var(--text-primary);
1820}
1821
1822.phase-card.selected .phase-card-label {
1823 color: var(--accent);
1824}
1825
1826.phase-card-desc {
1827 font-size: 11px;
1828 color: var(--text-secondary);
1829}
1830
1831.modal-footer {
1832 display: flex;
1833 justify-content: flex-end;
1834 gap: 8px;
1835 padding: 12px 16px;
1836 border-top: 1px solid var(--border);
1837}
1838
1839.modal-btn-secondary {
1840 background: transparent;
1841 border: 1px solid var(--border);
1842 border-radius: 2px;
1843 color: var(--text-secondary);
1844 font-size: 12px;
1845 padding: 5px 12px;
1846 cursor: pointer;
1847 font-family: inherit;
1848 transition: background 0.15s;
1849}
1850
1851.modal-btn-secondary:hover {
1852 background: var(--bg-tertiary);
1853 color: var(--text-primary);
1854}
1855
1856.modal-btn-primary {
1857 background: var(--accent);
1858 border: 1px solid var(--accent);
1859 border-radius: 2px;
1860 color: #fff;
1861 font-size: 12px;
1862 padding: 5px 12px;
1863 cursor: pointer;
1864 font-family: inherit;
1865 font-weight: 600;
1866 transition: background 0.15s;
1867}
1868
1869.modal-btn-primary:hover {
1870 background: var(--accent-hover);
1871 border-color: var(--accent-hover);
1872}
1873
1714.mcp-tools-empty { 1874.mcp-tools-empty {
1715 font-size: 11px; 1875 font-size: 11px;
1716 color: var(--text-secondary); 1876 color: var(--text-secondary);