fix(FN-673): split Model settings into Default Model and Execution Model sections
This commit is contained in:
@@ -23,8 +23,10 @@ import { applyPresetToSelection, generatePresetId, validatePresetId } from "../u
|
||||
*
|
||||
* Sections:
|
||||
* - general: Task prefix configuration (project)
|
||||
* - model: Default AI model selection (global)
|
||||
* - default-model: Default AI model selection (global)
|
||||
* - execution-model: Planning and validator model selection (project)
|
||||
* - model-presets: Reusable model presets (project)
|
||||
* - ai-summarization: Auto-summarization settings (project)
|
||||
* - appearance: Theme and color settings (global)
|
||||
* - scheduling: Concurrency, poll interval, file overlap serialization (project)
|
||||
* - worktrees: Worktree limits, init commands, recycling (project)
|
||||
@@ -35,7 +37,8 @@ import { applyPresetToSelection, generatePresetId, validatePresetId } from "../u
|
||||
*/
|
||||
const SETTINGS_SECTIONS = [
|
||||
{ id: "general", label: "General", scope: "project" as const },
|
||||
{ id: "model", label: "Model", scope: "global" as const },
|
||||
{ id: "default-model", label: "Default Model", scope: "global" as const },
|
||||
{ id: "execution-model", label: "Execution Model", scope: "project" as const },
|
||||
{ id: "model-presets", label: "Model Presets", scope: "project" as const },
|
||||
{ id: "ai-summarization", label: "AI Summarization", scope: "project" as const },
|
||||
{ id: "appearance", label: "Appearance", scope: "global" as const },
|
||||
@@ -134,7 +137,7 @@ export function SettingsModal({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === "model") {
|
||||
if (activeSection === "default-model" || activeSection === "execution-model") {
|
||||
setModelsLoading(true);
|
||||
fetchModels()
|
||||
.then((models) => setAvailableModels(models))
|
||||
@@ -353,9 +356,8 @@ export function SettingsModal({
|
||||
|
||||
// Always save both global and project settings.
|
||||
// The backend filters each appropriately (updateSettings ignores global keys,
|
||||
// updateGlobalSettings ignores project keys). This ensures fields in mixed-scope
|
||||
// sections like "model" (which has planningProvider/validatorProvider in project scope)
|
||||
// are persisted correctly.
|
||||
// updateGlobalSettings ignores project keys). This ensures fields in sections
|
||||
// are persisted correctly based on their scope.
|
||||
|
||||
const globalKeySet = new Set<string>(GLOBAL_SETTINGS_KEYS);
|
||||
const globalPatch: Partial<GlobalSettings> = {};
|
||||
@@ -489,10 +491,76 @@ export function SettingsModal({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "model": {
|
||||
case "default-model": {
|
||||
const selectedValue = form.defaultProvider && form.defaultModelId
|
||||
? `${form.defaultProvider}/${form.defaultModelId}`
|
||||
: "";
|
||||
return (
|
||||
<>
|
||||
{renderScopeBanner()}
|
||||
<h4 className="settings-section-heading">Default Model</h4>
|
||||
{modelsLoading ? (
|
||||
<div className="settings-empty-state">Loading available models…</div>
|
||||
) : availableModels.length === 0 ? (
|
||||
<div className="settings-empty-state settings-muted">
|
||||
No models available. Configure authentication first.
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label htmlFor="defaultModel">Default Model</label>
|
||||
<CustomModelDropdown
|
||||
id="defaultModel"
|
||||
label="Default Model"
|
||||
models={availableModels}
|
||||
value={selectedValue}
|
||||
onChange={(val) => {
|
||||
if (!val) {
|
||||
setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined }));
|
||||
} else {
|
||||
const slashIdx = val.indexOf("/");
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
defaultProvider: val.slice(0, slashIdx),
|
||||
defaultModelId: val.slice(slashIdx + 1),
|
||||
}));
|
||||
}
|
||||
}}
|
||||
placeholder="Use default"
|
||||
/>
|
||||
<small>Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.</small>
|
||||
</div>
|
||||
)}
|
||||
{(() => {
|
||||
const selectedModel = availableModels.find(
|
||||
(m) => m.provider === form.defaultProvider && m.id === form.defaultModelId,
|
||||
);
|
||||
if (selectedModel && !selectedModel.reasoning) return null;
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label htmlFor="defaultThinkingLevel">Thinking Effort</label>
|
||||
<select
|
||||
id="defaultThinkingLevel"
|
||||
value={form.defaultThinkingLevel || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm((f) => ({ ...f, defaultThinkingLevel: val || undefined } as any));
|
||||
}}
|
||||
>
|
||||
<option value="">Default</option>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<option key={level} value={level}>
|
||||
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<small>Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.</small>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case "execution-model": {
|
||||
const planningValue = form.planningProvider && form.planningModelId
|
||||
? `${form.planningProvider}/${form.planningModelId}`
|
||||
: "";
|
||||
@@ -502,7 +570,7 @@ export function SettingsModal({
|
||||
return (
|
||||
<>
|
||||
{renderScopeBanner()}
|
||||
<h4 className="settings-section-heading">Model</h4>
|
||||
<h4 className="settings-section-heading">Execution Model</h4>
|
||||
{modelsLoading ? (
|
||||
<div className="settings-empty-state">Loading available models…</div>
|
||||
) : availableModels.length === 0 ? (
|
||||
@@ -511,29 +579,6 @@ export function SettingsModal({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="defaultModel">Default Model</label>
|
||||
<CustomModelDropdown
|
||||
id="defaultModel"
|
||||
label="Default Model"
|
||||
models={availableModels}
|
||||
value={selectedValue}
|
||||
onChange={(val) => {
|
||||
if (!val) {
|
||||
setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined }));
|
||||
} else {
|
||||
const slashIdx = val.indexOf("/");
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
defaultProvider: val.slice(0, slashIdx),
|
||||
defaultModelId: val.slice(slashIdx + 1),
|
||||
}));
|
||||
}
|
||||
}}
|
||||
placeholder="Use default"
|
||||
/>
|
||||
<small>Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="planningModel">Planning Model</label>
|
||||
<CustomModelDropdown
|
||||
@@ -582,33 +627,6 @@ export function SettingsModal({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(() => {
|
||||
const selectedModel = availableModels.find(
|
||||
(m) => m.provider === form.defaultProvider && m.id === form.defaultModelId,
|
||||
);
|
||||
if (selectedModel && !selectedModel.reasoning) return null;
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label htmlFor="defaultThinkingLevel">Thinking Effort</label>
|
||||
<select
|
||||
id="defaultThinkingLevel"
|
||||
value={form.defaultThinkingLevel || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm((f) => ({ ...f, defaultThinkingLevel: val || undefined } as any));
|
||||
}}
|
||||
>
|
||||
<option value="">Default</option>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<option key={level} value={level}>
|
||||
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<small>Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.</small>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user