From d44d0f61e4026da35c0d1a4acb87ba71ed6cd599 Mon Sep 17 00:00:00 2001
From: Clawd <ai@clawd.bot>
Date: Sun, 1 Mar 2026 08:31:52 -0800
Subject: feat(settings): add configurable model selection

- Add Model settings section with free-text input for model override
- Pass configured model through to SDK query() calls
- Display active model badge in ActionBar next to token usage
- Seed model state from DB on mount, update from system:init events
- Empty/unset value uses SDK default (no breaking change)
---
 renderer/src/components/settings/ModelSettings.tsx | 94 ++++++++++++++++++++++
 1 file changed, 94 insertions(+)
 create mode 100644 renderer/src/components/settings/ModelSettings.tsx

(limited to 'renderer/src/components/settings/ModelSettings.tsx')

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 @@
+import React, { useState, useEffect } from "react";
+
+const api = window.api;
+
+export function ModelSettings() {
+  // null = not yet loaded from DB
+  const [model, setModel] = useState<string | null>(null);
+  const [draft, setDraft] = useState("");
+  const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
+
+  useEffect(() => {
+    api.getSettings(["model"]).then((settings) => {
+      const saved = settings["model"] ?? "";
+      setModel(saved);
+      setDraft(saved);
+    });
+  }, []);
+
+  const handleSave = async () => {
+    const trimmed = draft.trim();
+    if (trimmed) {
+      await api.setSetting("model", trimmed);
+      setModel(trimmed);
+    } else {
+      await api.deleteSetting("model");
+      setModel("");
+    }
+    setSaveStatus("saved");
+    setTimeout(() => setSaveStatus("idle"), 1500);
+  };
+
+  const handleReset = async () => {
+    await api.deleteSetting("model");
+    setModel("");
+    setDraft("");
+    setSaveStatus("saved");
+    setTimeout(() => setSaveStatus("idle"), 1500);
+  };
+
+  if (model === null) {
+    return (
+      <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
+        Loading...
+      </div>
+    );
+  }
+
+  const isDirty = draft.trim() !== model;
+
+  return (
+    <div>
+      <div className="settings-section-title">Model</div>
+      <div className="settings-section-desc">
+        Claude model to use for all phases. Leave blank to use the SDK&apos;s
+        default model. Takes effect on the next message sent in any session.
+      </div>
+
+      <div className="settings-toggle-row">
+        <input
+          className="settings-model-input"
+          type="text"
+          value={draft}
+          placeholder="Default (e.g. claude-sonnet-4-5)"
+          onChange={(e) => {
+            setDraft(e.target.value);
+            setSaveStatus("idle");
+          }}
+          onKeyDown={(e) => {
+            if (e.key === "Enter") handleSave();
+          }}
+          spellCheck={false}
+        />
+      </div>
+
+      <div className="settings-actions">
+        {model && (
+          <button className="btn-secondary" onClick={handleReset}>
+            Reset to Default
+          </button>
+        )}
+        <button
+          className="btn-primary"
+          onClick={handleSave}
+          disabled={!isDirty}
+        >
+          {saveStatus === "saved" ? "Saved \u2713" : "Save"}
+        </button>
+        {model && (
+          <span className="settings-custom-badge">custom</span>
+        )}
+      </div>
+    </div>
+  );
+}
-- 
cgit v1.2.3

