mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-24 05:22:22 +02:00
[eric] default-model + thinking-mode settings overhaul: Settings dropdown mirrors the in-session picker (provider-grouped from
state.models.byProvider, disambiguates same-named models by · Provider), toolbar now honors the stored default_model instead of snapping to stale Redux initial 'sonnet' and re-syncs on every reopen so last-used picks don't leak, smart fallback per priority Anthropic > OpenAI > Gemini > OpenSwarm Pro > OpenSwarm with Snackbar warning when the user's default becomes unreachable, new Default thinking mode setting (Auto/Off/Low/Medium/High) added to AppSettings and threaded into AgentSession on launch, onboarding flow for user intuitive-ness slight tweaks
This commit is contained in:
@@ -31,7 +31,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
: body.system;
|
||||
|
||||
systemContent = systemContent.replace(
|
||||
/You are Claude Code, Anthropic's official CLI for Claude\.\s*/,
|
||||
/^You are [^.]*?(?:Claude Code|Claude agent)[^.]*?\.\s*/,
|
||||
""
|
||||
);
|
||||
|
||||
|
||||
@@ -387,6 +387,7 @@ class AgentManager:
|
||||
max_turns=config.max_turns,
|
||||
cwd=effective_cwd,
|
||||
dashboard_id=config.dashboard_id,
|
||||
thinking_level=getattr(global_settings, "default_thinking_level", "auto"),
|
||||
)
|
||||
self.sessions[session_id] = session
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Any
|
||||
from typing import Optional, Any, Literal
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"You are a personal AI assistant running inside OpenSwarm.\n\n"
|
||||
@@ -37,6 +37,7 @@ class AppSettings(BaseModel):
|
||||
default_model: str = "sonnet"
|
||||
default_mode: str = "agent"
|
||||
default_max_turns: Optional[int] = None
|
||||
default_thinking_level: Literal["off", "low", "medium", "high", "auto"] = "auto"
|
||||
zoom_sensitivity: float = 50.0
|
||||
theme: str = "dark"
|
||||
new_agent_shortcut: str = "Meta+l"
|
||||
|
||||
+106
-2
@@ -1,10 +1,12 @@
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter, Routes, Route } from 'react-router-dom';
|
||||
import { ThemeProvider as MuiThemeProvider, createTheme, CssBaseline } from '@mui/material';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { store } from '../shared/state/store';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchSettings } from '@/shared/state/settingsSlice';
|
||||
import { fetchSettings, updateSettings } from '@/shared/state/settingsSlice';
|
||||
import { fetchModels } from '@/shared/state/modelsSlice';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import {
|
||||
@@ -186,6 +188,106 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Priority order for picking a default model when the user's stored
|
||||
// default_model is unreachable (no matching provider connected). The user's
|
||||
// preferred fallback ordering: direct provider keys first, then OpenSwarm
|
||||
// Pro, then Copilot-powered OpenSwarm free tier.
|
||||
const DEFAULT_MODEL_PRIORITY: string[] = [
|
||||
'Anthropic',
|
||||
'OpenAI',
|
||||
'Google',
|
||||
'OpenSwarm Pro',
|
||||
'OpenSwarm',
|
||||
];
|
||||
|
||||
// Preferred model pick inside each provider group. Ordered by the user's
|
||||
// stated preference: Sonnet mid-tier for Claude, GPT-5.4 Mini for OpenAI,
|
||||
// Flash for Gemini, and conservative picks for the shared tiers.
|
||||
const DEFAULT_MODEL_PICKS: Record<string, string[]> = {
|
||||
Anthropic: ['sonnet-cc', 'sonnet'],
|
||||
OpenAI: ['gpt-5.4-mini', 'gpt-5.4'],
|
||||
Google: ['gemini-2.5-flash', 'gemini-3-flash', 'gemini-2.5-pro'],
|
||||
'OpenSwarm Pro': ['sonnet', 'opus'],
|
||||
OpenSwarm: ['gpt-5-mini', 'claude-haiku-4.5', 'gpt-4.1'],
|
||||
};
|
||||
|
||||
function pickFallbackModel(
|
||||
byProvider: Record<string, Array<{ value: string; label: string }>>,
|
||||
): { value: string; label: string; provider: string } | null {
|
||||
for (const prov of DEFAULT_MODEL_PRIORITY) {
|
||||
const models = byProvider[prov];
|
||||
if (!models || models.length === 0) continue;
|
||||
const available = new Map(models.map((m) => [m.value, m]));
|
||||
const picks = DEFAULT_MODEL_PICKS[prov] || [];
|
||||
for (const candidate of picks) {
|
||||
const m = available.get(candidate);
|
||||
if (m) return { value: m.value, label: m.label, provider: prov };
|
||||
}
|
||||
const first = models[0];
|
||||
return { value: first.value, label: first.label, provider: prov };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reconciles the stored default_model against the set of models actually
|
||||
// reachable given the user's current connections. When the stored value is
|
||||
// unavailable, falls back per DEFAULT_MODEL_PRIORITY and shows a one-time
|
||||
// warning so the user knows why their default changed.
|
||||
const DefaultModelGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useAppSelector((s) => s.settings.data);
|
||||
const settingsLoaded = useAppSelector((s) => s.settings.loaded);
|
||||
const byProvider = useAppSelector((s) => s.models.byProvider);
|
||||
const modelsLoaded = useAppSelector((s) => s.models.loaded);
|
||||
|
||||
const [warning, setWarning] = useState<{ from: string; to: string; provider: string } | null>(null);
|
||||
const pendingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded || !modelsLoaded) return;
|
||||
if (pendingRef.current) return;
|
||||
if (Object.keys(byProvider).length === 0) return;
|
||||
|
||||
const flat = Object.values(byProvider).flat();
|
||||
const currentExists = flat.some((m) => m.value === settings.default_model);
|
||||
if (currentExists) return;
|
||||
|
||||
const fallback = pickFallbackModel(byProvider);
|
||||
if (!fallback || fallback.value === settings.default_model) return;
|
||||
|
||||
const fromLabel = flat.find((m) => m.value === settings.default_model)?.label ?? settings.default_model;
|
||||
pendingRef.current = true;
|
||||
dispatch(updateSettings({ ...settings, default_model: fallback.value }))
|
||||
.finally(() => {
|
||||
pendingRef.current = false;
|
||||
});
|
||||
setWarning({ from: fromLabel, to: fallback.label, provider: fallback.provider });
|
||||
}, [settingsLoaded, modelsLoaded, byProvider, settings, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={!!warning}
|
||||
autoHideDuration={8000}
|
||||
onClose={() => setWarning(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<Alert
|
||||
severity="warning"
|
||||
variant="filled"
|
||||
onClose={() => setWarning(null)}
|
||||
sx={{ fontSize: '0.8rem' }}
|
||||
>
|
||||
{warning && (
|
||||
<>Default model <b>{warning.from}</b> is no longer available — switched to <b>{warning.to}</b> ({warning.provider}).</>
|
||||
)}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UpdateListener: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -259,6 +361,7 @@ const ThemedApp: React.FC = () => {
|
||||
<HashRouter>
|
||||
<ShortcutsProvider>
|
||||
<SettingsLoader>
|
||||
<DefaultModelGuard>
|
||||
<UpdateListener>
|
||||
<DeepLinkListener>
|
||||
<Routes>
|
||||
@@ -280,6 +383,7 @@ const ThemedApp: React.FC = () => {
|
||||
<OnboardingModal />
|
||||
</DeepLinkListener>
|
||||
</UpdateListener>
|
||||
</DefaultModelGuard>
|
||||
</SettingsLoader>
|
||||
</ShortcutsProvider>
|
||||
</HashRouter>
|
||||
|
||||
@@ -37,8 +37,8 @@ const streamingCursorKeyframes = `
|
||||
// wave traveling through the letters.
|
||||
const thinkingShimmerKeyframes = `
|
||||
@keyframes thinking-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -154,11 +154,13 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [newAgentBounce, setNewAgentBounce] = useState(false);
|
||||
|
||||
const handleWalkthroughComplete = useCallback(() => {
|
||||
setShowWalkthrough(false);
|
||||
localStorage.removeItem('openswarm_walkthrough_pending');
|
||||
localStorage.setItem('openswarm_walkthrough_seen', 'true');
|
||||
setNewAgentBounce(true);
|
||||
}, []);
|
||||
|
||||
const handleHighlightCard = useCallback((cardId: string) => {
|
||||
@@ -1657,11 +1659,23 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<style>{`@keyframes empty-state-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }`}</style>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '1.1rem', mb: 1 }}>
|
||||
No agents running
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.9rem' }}>
|
||||
Click "New Agent" to launch your first Claude Code instance
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
background: `linear-gradient(90deg, ${c.text.ghost} 0%, ${c.text.ghost} 40%, ${c.text.primary} 50%, ${c.text.ghost} 60%, ${c.text.ghost} 100%)`,
|
||||
backgroundSize: '200% 100%',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
color: 'transparent',
|
||||
animation: 'empty-state-shimmer 6s linear infinite',
|
||||
}}
|
||||
>
|
||||
Click the "+" button below to launch your first agent
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -1956,6 +1970,8 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
onHistoryResume={handleHistoryResume}
|
||||
onAddBrowser={handleAddBrowser}
|
||||
dashboardId={dashboardId}
|
||||
newAgentBounce={newAgentBounce}
|
||||
onNewAgentBounceEnd={() => setNewAgentBounce(false)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ interface Props {
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
onAddBrowser: () => void;
|
||||
dashboardId?: string;
|
||||
newAgentBounce?: boolean;
|
||||
onNewAgentBounceEnd?: () => void;
|
||||
}
|
||||
|
||||
const TOOLBAR_OWNER_ID = '__toolbar__';
|
||||
@@ -83,7 +85,7 @@ function formatRelativeTime(dateStr: string | null): string {
|
||||
}
|
||||
|
||||
const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, dashboardId }, ref) => {
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, dashboardId, newAgentBounce, onNewAgentBounceEnd }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const elementSelection = useElementSelection();
|
||||
@@ -93,17 +95,37 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
const historyListRef = useRef<HTMLDivElement>(null);
|
||||
const defaultMode = useAppSelector((s) => s.settings.data.default_mode);
|
||||
const defaultModel = useAppSelector((s) => s.settings.data.default_model);
|
||||
const defaultThinkingLevel = useAppSelector((s) => s.settings.data.default_thinking_level);
|
||||
const settingsLoaded = useAppSelector((s) => s.settings.loaded);
|
||||
const [mode, setMode] = useState(defaultMode || 'agent');
|
||||
const [model, setModel] = useState(defaultModel || 'sonnet');
|
||||
const [thinkingLevel, setThinkingLevel] = useState<'off' | 'low' | 'medium' | 'high' | 'auto'>('auto');
|
||||
const [thinkingLevel, setThinkingLevel] = useState<'off' | 'low' | 'medium' | 'high' | 'auto'>(defaultThinkingLevel || 'auto');
|
||||
// Snap to the persisted Settings defaults as soon as they arrive from the
|
||||
// backend. Without the settingsLoaded guard, the effect fires against the
|
||||
// Redux initialState ('sonnet') before the real default has loaded, and
|
||||
// the settingsApplied flag then locks out the real default for the rest
|
||||
// of the session — so new chats spawn under the stale value.
|
||||
const settingsApplied = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!settingsApplied.current) {
|
||||
if (settingsLoaded && !settingsApplied.current) {
|
||||
setMode(defaultMode || 'agent');
|
||||
setModel(defaultModel || 'sonnet');
|
||||
setThinkingLevel(defaultThinkingLevel || 'auto');
|
||||
settingsApplied.current = true;
|
||||
}
|
||||
}, [defaultMode, defaultModel]);
|
||||
}, [settingsLoaded, defaultMode, defaultModel, defaultThinkingLevel]);
|
||||
// Reset to the current Settings defaults each time the toolbar reopens
|
||||
// for a new compose session, so the user's in-session model/mode picks
|
||||
// don't leak into the next new-chat draft.
|
||||
const prevInputOpen = useRef(false);
|
||||
useEffect(() => {
|
||||
if (settingsLoaded && inputOpen && !prevInputOpen.current) {
|
||||
setMode(defaultMode || 'agent');
|
||||
setModel(defaultModel || 'sonnet');
|
||||
setThinkingLevel(defaultThinkingLevel || 'auto');
|
||||
}
|
||||
prevInputOpen.current = inputOpen;
|
||||
}, [inputOpen, settingsLoaded, defaultMode, defaultModel, defaultThinkingLevel]);
|
||||
const [viewPickerOpen, setViewPickerOpen] = useState(false);
|
||||
const [viewSearch, setViewSearch] = useState('');
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
@@ -597,6 +619,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
data-onboarding="new-agent-button"
|
||||
tabIndex={0}
|
||||
onClick={onNewAgent}
|
||||
onAnimationEnd={newAgentBounce ? onNewAgentBounceEnd : undefined}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -610,6 +633,13 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
transition: 'background-color 0.15s',
|
||||
'&:hover': { bgcolor: c.accent.hover },
|
||||
'&:active': { bgcolor: c.accent.pressed },
|
||||
...(newAgentBounce && {
|
||||
animation: 'new-agent-bounce 0.7s ease-in-out 4',
|
||||
'@keyframes new-agent-bounce': {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-8px)' },
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 20 }} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
@@ -57,6 +58,29 @@ import type { OpenSwarmPlan } from '@/shared/subscription/checkout';
|
||||
// through 9Router's `github` OAuth under the generic SubscriptionCard path
|
||||
// below, so the dead component was removed.
|
||||
|
||||
// Brand colors for provider group headers in the default-model picker.
|
||||
// Mirrors the set used by the in-session ChatInput picker.
|
||||
const PROVIDER_COLORS: Record<string, string> = {
|
||||
anthropic: '#E8927A',
|
||||
openai: '#74AA9C',
|
||||
google: '#4285F4',
|
||||
gemini: '#4285F4',
|
||||
xai: '#8B949E',
|
||||
meta: '#0866FF',
|
||||
deepseek: '#4D6BFE',
|
||||
mistral: '#FF7000',
|
||||
qwen: '#A974FF',
|
||||
cohere: '#FF7759',
|
||||
};
|
||||
const OPENSWARM_GRADIENT =
|
||||
'linear-gradient(135deg, #8FB3FF 0%, #E56BC4 45%, #FFA85C 100%)';
|
||||
|
||||
const DEFAULT_MODEL_FALLBACK = [
|
||||
{ value: 'sonnet', label: 'Claude Sonnet 4.6' },
|
||||
{ value: 'opus', label: 'Claude Opus 4.6' },
|
||||
{ value: 'haiku', label: 'Claude Haiku 4.5' },
|
||||
];
|
||||
|
||||
// ── Subscription Provider Card ──
|
||||
const SUBSCRIPTION_PROVIDERS = [
|
||||
{ id: 'claude', name: 'Claude Pro / Max', desc: 'Sonnet 4.6, Opus 4.6, Haiku 4.5', color: '#E8927A', preview: false },
|
||||
@@ -1093,6 +1117,29 @@ const Settings: React.FC = () => {
|
||||
|
||||
const modesList = useMemo(() => Object.values(modes), [modes]);
|
||||
|
||||
// Model picker source — same state as the in-session ChatInput picker, so
|
||||
// Settings shows exactly the models gated-in by the user's connected
|
||||
// providers / subscriptions (OpenSwarm Pro, Anthropic, OpenAI, Google, ...).
|
||||
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
|
||||
const modelsLoaded = useAppSelector((s) => s.models.loaded);
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
if (!modelsLoaded || Object.keys(modelsByProvider).length === 0) {
|
||||
const key = settings.connection_mode === 'openswarm-pro' ? 'OpenSwarm Pro' : 'Anthropic';
|
||||
return {
|
||||
grouped: { [key]: DEFAULT_MODEL_FALLBACK },
|
||||
flat: DEFAULT_MODEL_FALLBACK.map((m) => ({ ...m, provider: key })),
|
||||
};
|
||||
}
|
||||
const grouped: Record<string, Array<{ value: string; label: string }>> = {};
|
||||
const flat: Array<{ value: string; label: string; provider: string }> = [];
|
||||
for (const [prov, models] of Object.entries(modelsByProvider)) {
|
||||
grouped[prov] = models.map((m) => ({ value: m.value, label: m.label }));
|
||||
for (const m of models) flat.push({ value: m.value, label: m.label, provider: prov });
|
||||
}
|
||||
return { grouped, flat };
|
||||
}, [modelsByProvider, modelsLoaded, settings.connection_mode]);
|
||||
|
||||
const updateStatus = useAppSelector((s) => s.update.status);
|
||||
const appVersion = useAppSelector((s) => s.update.appVersion);
|
||||
const availableVersion = useAppSelector((s) => s.update.availableVersion);
|
||||
@@ -1122,6 +1169,10 @@ const Settings: React.FC = () => {
|
||||
dispatch(fetchModes());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) dispatch(fetchModels());
|
||||
}, [open, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset to the General tab on open, but NOT when the caller has
|
||||
// explicitly requested a tab via openSettingsModal(<tab>) — e.g. the
|
||||
@@ -1416,16 +1467,78 @@ const Settings: React.FC = () => {
|
||||
<Typography sx={labelSx}>Model</Typography>
|
||||
<Typography sx={descSx}>Default model for new sessions.</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 170 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<Select
|
||||
value={form.default_model}
|
||||
onChange={(e) => setForm({ ...form, default_model: e.target.value })}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
renderValue={(val) => {
|
||||
const m = modelOptions.flat.find((x) => x.value === val);
|
||||
if (!m) return String(val);
|
||||
return (
|
||||
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<span>{m.label}</span>
|
||||
<Typography component="span" sx={{ fontSize: '0.65rem', color: c.text.ghost }}>
|
||||
· {m.provider}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="sonnet">Sonnet 4.6</MenuItem>
|
||||
<MenuItem value="opus">Opus 4.6</MenuItem>
|
||||
<MenuItem value="haiku">Haiku 3.5</MenuItem>
|
||||
{Object.entries(modelOptions.grouped).flatMap(([prov, models]) => {
|
||||
const isOpenSwarmPro = prov === 'OpenSwarm Pro';
|
||||
const brandColor = PROVIDER_COLORS[prov.toLowerCase()] ?? c.text.tertiary;
|
||||
return [
|
||||
<ListSubheader
|
||||
key={`header-${prov}`}
|
||||
sx={{
|
||||
bgcolor: c.bg.surface,
|
||||
lineHeight: '1.8em',
|
||||
px: 1.5,
|
||||
py: 0.4,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0,
|
||||
background: isOpenSwarmPro ? OPENSWARM_GRADIENT : brandColor,
|
||||
boxShadow: isOpenSwarmPro
|
||||
? '0 0 8px rgba(229, 107, 196, 0.6)'
|
||||
: `0 0 6px ${brandColor}80`,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
...(isOpenSwarmPro
|
||||
? {
|
||||
background: OPENSWARM_GRADIENT,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}
|
||||
: { color: brandColor }),
|
||||
}}
|
||||
>
|
||||
{prov}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListSubheader>,
|
||||
...models.map((m) => (
|
||||
<MenuItem key={m.value} value={m.value} sx={{ fontSize: '0.85rem', pl: 3 }}>
|
||||
{m.label}
|
||||
</MenuItem>
|
||||
)),
|
||||
];
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
@@ -1449,6 +1562,27 @@ const Settings: React.FC = () => {
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Thinking</Typography>
|
||||
<Typography sx={descSx}>Default thinking level for reasoning-capable models.</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 170 }}>
|
||||
<Select
|
||||
value={form.default_thinking_level}
|
||||
onChange={(e) => setForm({ ...form, default_thinking_level: e.target.value as AppSettings['default_thinking_level'] })}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
>
|
||||
<MenuItem value="auto">Auto</MenuItem>
|
||||
<MenuItem value="off">Off</MenuItem>
|
||||
<MenuItem value="low">Low</MenuItem>
|
||||
<MenuItem value="medium">Medium</MenuItem>
|
||||
<MenuItem value="high">High</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Max turns</Typography>
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface AppSettings {
|
||||
default_model: string;
|
||||
default_mode: string;
|
||||
default_max_turns: number | null;
|
||||
default_thinking_level: 'off' | 'low' | 'medium' | 'high' | 'auto';
|
||||
zoom_sensitivity: number;
|
||||
theme: 'light' | 'dark';
|
||||
new_agent_shortcut: string;
|
||||
@@ -92,6 +93,7 @@ const initialState: SettingsState = {
|
||||
default_model: 'sonnet',
|
||||
default_mode: 'agent',
|
||||
default_max_turns: null,
|
||||
default_thinking_level: 'auto',
|
||||
zoom_sensitivity: 50,
|
||||
theme: 'dark',
|
||||
new_agent_shortcut: 'Meta+l',
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user