aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src')
-rw-r--r--renderer/src/App.tsx15
-rw-r--r--renderer/src/components/ActionBar.tsx9
-rw-r--r--renderer/src/components/SettingsPage.tsx14
-rw-r--r--renderer/src/components/settings/ModelSettings.tsx94
-rw-r--r--renderer/src/styles/globals.css29
5 files changed, 158 insertions, 3 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index 36d3a82..b2cd168 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -59,6 +59,7 @@ export function App() {
59 }); 59 });
60 const [error, setError] = useState<string | null>(null); 60 const [error, setError] = useState<string | null>(null);
61 const [showSettings, setShowSettings] = useState(false); 61 const [showSettings, setShowSettings] = useState(false);
62 const [activeModel, setActiveModel] = useState<string | null>(null);
62 63
63 const [theme, setTheme] = useState<Theme>( 64 const [theme, setTheme] = useState<Theme>(
64 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" 65 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark"
@@ -147,9 +148,14 @@ export function App() {
147 return () => window.removeEventListener("keydown", handleKeyDown); 148 return () => window.removeEventListener("keydown", handleKeyDown);
148 }, [selectedSession, isLoading]); 149 }, [selectedSession, isLoading]);
149 150
150 // Load projects on mount 151 // Load projects and initial model setting on mount
151 useEffect(() => { 152 useEffect(() => {
152 api.listProjects().then(setProjects); 153 api.listProjects().then(setProjects);
154 // Seed the model badge from the DB so it shows before any query fires.
155 // system:init will overwrite this with the SDK-resolved name once a query runs.
156 api.getSettings(["model"]).then((s) => {
157 if (s["model"]) setActiveModel(s["model"]);
158 });
153 }, []); 159 }, []);
154 160
155 // Load sessions when project changes 161 // Load sessions when project changes
@@ -209,6 +215,12 @@ export function App() {
209 ); 215 );
210 } 216 }
211 217
218 // ── Model resolved by SDK ────────────────────────────────────────
219 // SDKSystemMessage (subtype "init") contains the actual model in use.
220 if (msg.type === "system" && msg.subtype === "init") {
221 setActiveModel((msg as { model?: string }).model ?? null);
222 }
223
212 // ── Result (success or error) ──────────────────────────────────── 224 // ── Result (success or error) ────────────────────────────────────
213 // Always clear loading state on any result subtype so error results 225 // Always clear loading state on any result subtype so error results
214 // don't leave the UI stuck in the loading/thinking state. 226 // don't leave the UI stuck in the loading/thinking state.
@@ -498,6 +510,7 @@ export function App() {
498 } 510 }
499 }} 511 }}
500 disabled={!selectedSession} 512 disabled={!selectedSession}
513 modelName={activeModel}
501 /> 514 />
502 515
503 {showSettings && ( 516 {showSettings && (
diff --git a/renderer/src/components/ActionBar.tsx b/renderer/src/components/ActionBar.tsx
index 22f34b4..d270583 100644
--- a/renderer/src/components/ActionBar.tsx
+++ b/renderer/src/components/ActionBar.tsx
@@ -11,6 +11,7 @@ interface ActionBarProps {
11 onSubmit: () => void; 11 onSubmit: () => void;
12 onPermissionModeChange: (mode: PermissionMode) => void; 12 onPermissionModeChange: (mode: PermissionMode) => void;
13 disabled: boolean; 13 disabled: boolean;
14 modelName?: string | null;
14} 15}
15 16
16export function ActionBar({ 17export function ActionBar({
@@ -23,6 +24,7 @@ export function ActionBar({
23 onSubmit, 24 onSubmit,
24 onPermissionModeChange, 25 onPermissionModeChange,
25 disabled, 26 disabled,
27 modelName,
26}: ActionBarProps) { 28}: ActionBarProps) {
27 const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens; 29 const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
28 const maxTokens = 200000; 30 const maxTokens = 200000;
@@ -52,6 +54,13 @@ export function ActionBar({
52 </span> 54 </span>
53 </div> 55 </div>
54 56
57 {/* ── Model badge ── */}
58 {modelName && (
59 <span className="model-badge" title="Active model">
60 {modelName}
61 </span>
62 )}
63
55 {phase === "implement" && ( 64 {phase === "implement" && (
56 <label className="permission-toggle"> 65 <label className="permission-toggle">
57 <input 66 <input
diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx
index 9ebde44..d3ff4bf 100644
--- a/renderer/src/components/SettingsPage.tsx
+++ b/renderer/src/components/SettingsPage.tsx
@@ -1,8 +1,9 @@
1import React, { useState } from "react"; 1import React, { useState } from "react";
2import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; 2import { SystemPromptsSettings } from "./settings/SystemPromptsSettings";
3import { GitSettings } from "./settings/GitSettings"; 3import { GitSettings } from "./settings/GitSettings";
4import { ModelSettings } from "./settings/ModelSettings";
4 5
5type SettingsSection = "system-prompts" | "git"; 6type SettingsSection = "model" | "system-prompts" | "git";
6 7
7interface SettingsPageProps { 8interface SettingsPageProps {
8 onClose: () => void; 9 onClose: () => void;
@@ -10,7 +11,7 @@ interface SettingsPageProps {
10 11
11export function SettingsPage({ onClose }: SettingsPageProps) { 12export function SettingsPage({ onClose }: SettingsPageProps) {
12 const [activeSection, setActiveSection] = 13 const [activeSection, setActiveSection] =
13 useState<SettingsSection>("system-prompts"); 14 useState<SettingsSection>("model");
14 15
15 return ( 16 return (
16 <div className="settings-overlay"> 17 <div className="settings-overlay">
@@ -33,6 +34,14 @@ export function SettingsPage({ onClose }: SettingsPageProps) {
33 <nav className="settings-nav"> 34 <nav className="settings-nav">
34 <button 35 <button
35 className={`settings-nav-item${ 36 className={`settings-nav-item${
37 activeSection === "model" ? " active" : ""
38 }`}
39 onClick={() => setActiveSection("model")}
40 >
41 Model
42 </button>
43 <button
44 className={`settings-nav-item${
36 activeSection === "system-prompts" ? " active" : "" 45 activeSection === "system-prompts" ? " active" : ""
37 }`} 46 }`}
38 onClick={() => setActiveSection("system-prompts")} 47 onClick={() => setActiveSection("system-prompts")}
@@ -51,6 +60,7 @@ export function SettingsPage({ onClose }: SettingsPageProps) {
51 60
52 {/* Content */} 61 {/* Content */}
53 <div className="settings-content"> 62 <div className="settings-content">
63 {activeSection === "model" && <ModelSettings />}
54 {activeSection === "system-prompts" && <SystemPromptsSettings />} 64 {activeSection === "system-prompts" && <SystemPromptsSettings />}
55 {activeSection === "git" && <GitSettings />} 65 {activeSection === "git" && <GitSettings />}
56 </div> 66 </div>
diff --git a/renderer/src/components/settings/ModelSettings.tsx b/renderer/src/components/settings/ModelSettings.tsx
new file mode 100644
index 0000000..ecfc12c
--- /dev/null
+++ b/renderer/src/components/settings/ModelSettings.tsx
@@ -0,0 +1,94 @@
1import React, { useState, useEffect } from "react";
2
3const api = window.api;
4
5export function ModelSettings() {
6 // null = not yet loaded from DB
7 const [model, setModel] = useState<string | null>(null);
8 const [draft, setDraft] = useState("");
9 const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
10
11 useEffect(() => {
12 api.getSettings(["model"]).then((settings) => {
13 const saved = settings["model"] ?? "";
14 setModel(saved);
15 setDraft(saved);
16 });
17 }, []);
18
19 const handleSave = async () => {
20 const trimmed = draft.trim();
21 if (trimmed) {
22 await api.setSetting("model", trimmed);
23 setModel(trimmed);
24 } else {
25 await api.deleteSetting("model");
26 setModel("");
27 }
28 setSaveStatus("saved");
29 setTimeout(() => setSaveStatus("idle"), 1500);
30 };
31
32 const handleReset = async () => {
33 await api.deleteSetting("model");
34 setModel("");
35 setDraft("");
36 setSaveStatus("saved");
37 setTimeout(() => setSaveStatus("idle"), 1500);
38 };
39
40 if (model === null) {
41 return (
42 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
43 Loading...
44 </div>
45 );
46 }
47
48 const isDirty = draft.trim() !== model;
49
50 return (
51 <div>
52 <div className="settings-section-title">Model</div>
53 <div className="settings-section-desc">
54 Claude model to use for all phases. Leave blank to use the SDK&apos;s
55 default model. Takes effect on the next message sent in any session.
56 </div>
57
58 <div className="settings-toggle-row">
59 <input
60 className="settings-model-input"
61 type="text"
62 value={draft}
63 placeholder="Default (e.g. claude-sonnet-4-5)"
64 onChange={(e) => {
65 setDraft(e.target.value);
66 setSaveStatus("idle");
67 }}
68 onKeyDown={(e) => {
69 if (e.key === "Enter") handleSave();
70 }}
71 spellCheck={false}
72 />
73 </div>
74
75 <div className="settings-actions">
76 {model && (
77 <button className="btn-secondary" onClick={handleReset}>
78 Reset to Default
79 </button>
80 )}
81 <button
82 className="btn-primary"
83 onClick={handleSave}
84 disabled={!isDirty}
85 >
86 {saveStatus === "saved" ? "Saved \u2713" : "Save"}
87 </button>
88 {model && (
89 <span className="settings-custom-badge">custom</span>
90 )}
91 </div>
92 </div>
93 );
94}
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index 7a8ae45..0db62c2 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -621,6 +621,17 @@ html[data-theme="light"] .chat-input input:focus {
621 color: var(--text-secondary); 621 color: var(--text-secondary);
622} 622}
623 623
624.model-badge {
625 font-size: 11px;
626 font-family: var(--font-mono, monospace);
627 color: var(--text-secondary);
628 padding: 2px 6px;
629 border: 1px solid var(--border);
630 border-radius: 4px;
631 white-space: nowrap;
632 user-select: none;
633}
634
624.permission-toggle { 635.permission-toggle {
625 display: flex; 636 display: flex;
626 align-items: center; 637 align-items: center;
@@ -953,6 +964,24 @@ html[data-theme="light"] .settings-textarea:focus {
953 border-color: var(--warning); 964 border-color: var(--warning);
954} 965}
955 966
967/* ── Model Input ─────────────────────────────────────────────── */
968.settings-model-input {
969 width: 100%;
970 padding: 6px 8px;
971 background: var(--bg-secondary);
972 border: 1px solid var(--border);
973 border-radius: 4px;
974 color: var(--text-primary);
975 font-size: 13px;
976 font-family: var(--font-mono, monospace);
977 box-sizing: border-box;
978 transition: border-color 0.15s;
979}
980.settings-model-input:focus {
981 outline: none;
982 border-color: var(--accent);
983}
984
956/* ── Settings Actions Row ────────────────────────────────────── */ 985/* ── Settings Actions Row ────────────────────────────────────── */
957.settings-actions { 986.settings-actions {
958 display: flex; 987 display: flex;