From 73d2680b83ccbdbd8dfec2d319533e98b379b830 Mon Sep 17 00:00:00 2001 From: bndw Date: Wed, 4 Mar 2026 21:36:32 -0800 Subject: feat: Thread optional `phase` param into `db/sessions.ts::cre… (+7 more) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ 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` --- renderer/src/App.tsx | 30 +++- renderer/src/components/DocumentPane.tsx | 15 +- renderer/src/components/NewSessionModal.tsx | 60 ++++++++ renderer/src/components/settings/ModelSettings.tsx | 156 ++++++++++++++++++-- renderer/src/styles/globals.css | 160 +++++++++++++++++++++ 5 files changed, 402 insertions(+), 19 deletions(-) create mode 100644 renderer/src/components/NewSessionModal.tsx (limited to 'renderer/src') 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"; import { ChatPane } from "./components/ChatPane"; import { ActionBar } from "./components/ActionBar"; import { SettingsPage } from "./components/SettingsPage"; +import { NewSessionModal } from "./components/NewSessionModal"; import type { Project, Session, Message, Phase, TokenUsage } from "./types"; import "./styles/globals.css"; @@ -66,6 +67,8 @@ export function App() { }); const [error, setError] = useState(null); const [showSettings, setShowSettings] = useState(false); + const [showNewSessionModal, setShowNewSessionModal] = useState(false); + const [newSessionProjectId, setNewSessionProjectId] = useState(null); const [activeModel, setActiveModel] = useState(null); const [theme, setTheme] = useState( @@ -451,12 +454,25 @@ export function App() { setSelectedProject(project); }; - const handleCreateSession = async (projectId: string) => { + // Called by Sidebar when user clicks "+" — opens the phase-selection modal + const handleCreateSession = (projectId: string) => { + setNewSessionProjectId(projectId); + setShowNewSessionModal(true); + }; + + // Called by NewSessionModal when user clicks "Create" + const handleConfirmNewSession = async (phase: Phase) => { + setShowNewSessionModal(false); + const projectId = newSessionProjectId; + setNewSessionProjectId(null); + if (!projectId) return; + const project = projects.find((p) => p.id === projectId); if (!project) return; + const projectSessions = sessions.filter((s) => s.project_id === projectId); const name = `Session ${projectSessions.length + 1}`; - const session = await api.createSession(projectId, name); + const session = await api.createSession(projectId, name, phase); setSessions((prev) => [session, ...prev]); setSelectedProject(project); setSelectedSession(session); @@ -623,6 +639,16 @@ export function App() { {showSettings && ( setShowSettings(false)} /> )} + + {showNewSessionModal && ( + { + setShowNewSessionModal(false); + setNewSessionProjectId(null); + }} + /> + )} ); } 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({ }: DocumentPaneProps) { const [isEditing, setIsEditing] = useState(false); + // Always exit edit mode when the pane becomes read-only. + useEffect(() => { + if (disabled) setIsEditing(false); + }, [disabled]); + if (showOnboarding) { return (
@@ -206,9 +211,13 @@ export function DocumentPane({
{filename} - + {disabled ? ( + Read-only + ) : ( + + )}
{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 @@ +import React, { useState, useEffect } from "react"; +import type { Phase } from "../types"; + +interface NewSessionModalProps { + onConfirm: (phase: Phase) => void; + onCancel: () => void; +} + +const phaseOptions: { phase: Phase; label: string; description: string }[] = [ + { phase: "research", label: "Research", description: "Understand the codebase and requirements" }, + { phase: "plan", label: "Plan", description: "Create a detailed implementation strategy" }, + { phase: "implement", label: "Implement", description: "Execute the implementation plan" }, +]; + +export function NewSessionModal({ onConfirm, onCancel }: NewSessionModalProps) { + const [selected, setSelected] = useState("research"); + + // Close on Escape + useEffect(() => { + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onCancel]); + + return ( +
+
e.stopPropagation()}> + +
+ New Session + +
+ +
+

Choose where to start

+
+ {phaseOptions.map(({ phase, label, description }) => ( + + ))} +
+
+ +
+ + +
+ +
+
+ ); +} 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 @@ import React, { useState, useEffect } from "react"; +import type { Phase } from "../../types"; const api = window.api; +const PHASES: Phase[] = ["research", "plan", "implement"]; +const PHASE_LABELS: Record = { + research: "Research", + plan: "Plan", + implement: "Implement", +}; + export function ModelSettings() { - // null = not yet loaded from DB + // ── Global default ─────────────────────────────────────────── const [model, setModel] = useState(null); const [draft, setDraft] = useState(""); const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle"); + // ── Per-phase overrides ────────────────────────────────────── + const [phaseModels, setPhaseModels] = useState>({ + research: null, + plan: null, + implement: null, + }); + const [phaseDrafts, setPhaseDrafts] = useState>({ + research: "", + plan: "", + implement: "", + }); + const [phaseStatus, setPhaseStatus] = useState>({ + research: "idle", + plan: "idle", + implement: "idle", + }); + useEffect(() => { - api.getSettings(["model"]).then((settings) => { - const saved = settings["model"] ?? ""; - setModel(saved); - setDraft(saved); - }); + api + .getSettings(["model", "model.research", "model.plan", "model.implement"]) + .then((settings) => { + const saved = settings["model"] ?? ""; + setModel(saved); + setDraft(saved); + + const pm: Record = { + research: settings["model.research"], + plan: settings["model.plan"], + implement: settings["model.implement"], + }; + setPhaseModels(pm); + setPhaseDrafts({ + research: pm.research ?? "", + plan: pm.plan ?? "", + implement: pm.implement ?? "", + }); + }); }, []); + // ── Global handlers ────────────────────────────────────────── const handleSave = async () => { const trimmed = draft.trim(); if (trimmed) { @@ -37,6 +77,34 @@ export function ModelSettings() { setTimeout(() => setSaveStatus("idle"), 1500); }; + // ── Per-phase handlers ─────────────────────────────────────── + const handlePhaseSave = async (phase: Phase) => { + const trimmed = phaseDrafts[phase].trim(); + if (trimmed) { + await api.setSetting(`model.${phase}`, trimmed); + setPhaseModels((prev) => ({ ...prev, [phase]: trimmed })); + } else { + await api.deleteSetting(`model.${phase}`); + setPhaseModels((prev) => ({ ...prev, [phase]: null })); + } + setPhaseStatus((prev) => ({ ...prev, [phase]: "saved" })); + setTimeout( + () => setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" })), + 1500 + ); + }; + + const handlePhaseReset = async (phase: Phase) => { + await api.deleteSetting(`model.${phase}`); + setPhaseModels((prev) => ({ ...prev, [phase]: null })); + setPhaseDrafts((prev) => ({ ...prev, [phase]: "" })); + setPhaseStatus((prev) => ({ ...prev, [phase]: "saved" })); + setTimeout( + () => setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" })), + 1500 + ); + }; + if (model === null) { return (
@@ -49,10 +117,11 @@ export function ModelSettings() { return (
-
Model
+ {/* ── Global default ──────────────────────────────────── */} +
Default Model
- Claude model to use for all phases. Leave blank to use the SDK's - default model. Takes effect on the next message sent in any session. + Fallback model used for all phases unless a per-phase override is set. + Leave blank to use the SDK's built-in default.
@@ -60,7 +129,7 @@ export function ModelSettings() { className="settings-model-input" type="text" value={draft} - placeholder="Default (e.g. claude-sonnet-4-5)" + placeholder="e.g. claude-sonnet-4-5" onChange={(e) => { setDraft(e.target.value); setSaveStatus("idle"); @@ -75,7 +144,7 @@ export function ModelSettings() {
{model && ( )} - {model && ( - custom - )} + {model && custom} +
+ + {/* ── Per-phase overrides ─────────────────────────────── */} +
+ Phase Overrides +
+
+ Use a different model for a specific phase. Leave blank to inherit the + default above. Takes effect on the next message in any session. +
+ +
+ {PHASES.map((phase) => { + const saved = phaseModels[phase]; + const phaseDirty = phaseDrafts[phase].trim() !== (saved ?? ""); + + return ( +
+ + {PHASE_LABELS[phase]} + + { + setPhaseDrafts((prev) => ({ + ...prev, + [phase]: e.target.value, + })); + setPhaseStatus((prev) => ({ ...prev, [phase]: "idle" })); + }} + onKeyDown={(e) => { + if (e.key === "Enter") handlePhaseSave(phase); + }} + spellCheck={false} + /> +
+ {saved && ( + + )} + + {saved && ( + custom + )} +
+
+ ); + })}
); 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 { margin-left: 4px; } +/* ── New Session Modal ───────────────────────────────────────── */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 4px; + width: 400px; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); +} + +.modal-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.modal-close { + background: transparent; + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-secondary); + font-size: 14px; + width: 22px; + height: 22px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; +} + +.modal-close:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.modal-body { + padding: 20px 16px 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.modal-prompt { + font-size: 12px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.phase-cards { + display: flex; + flex-direction: column; + gap: 6px; +} + +.phase-card { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + padding: 10px 12px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 3px; + cursor: pointer; + text-align: left; + transition: border-color 0.15s, background 0.15s; + width: 100%; +} + +.phase-card:hover { + border-color: var(--accent); + background: var(--bg-tertiary); +} + +.phase-card.selected { + border-color: var(--accent); + background: var(--bg-tertiary); +} + +.phase-card-label { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} + +.phase-card.selected .phase-card-label { + color: var(--accent); +} + +.phase-card-desc { + font-size: 11px; + color: var(--text-secondary); +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--border); +} + +.modal-btn-secondary { + background: transparent; + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-secondary); + font-size: 12px; + padding: 5px 12px; + cursor: pointer; + font-family: inherit; + transition: background 0.15s; +} + +.modal-btn-secondary:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.modal-btn-primary { + background: var(--accent); + border: 1px solid var(--accent); + border-radius: 2px; + color: #fff; + font-size: 12px; + padding: 5px 12px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + transition: background 0.15s; +} + +.modal-btn-primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + .mcp-tools-empty { font-size: 11px; color: var(--text-secondary); -- cgit v1.2.3