diff --git a/ui/src/pages/settings/components/tab-navigation.tsx b/ui/src/pages/settings/components/tab-navigation.tsx index a318d81d..fb203163 100644 --- a/ui/src/pages/settings/components/tab-navigation.tsx +++ b/ui/src/pages/settings/components/tab-navigation.tsx @@ -4,7 +4,7 @@ */ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Globe, Settings2, Server, KeyRound } from 'lucide-react'; +import { Globe, Settings2, Server, KeyRound, Brain } from 'lucide-react'; import type { SettingsTab } from '../types'; interface TabNavigationProps { @@ -24,6 +24,10 @@ export function TabNavigation({ activeTab, onTabChange }: TabNavigationProps) { Global Env + + + Thinking + Proxy diff --git a/ui/src/pages/settings/hooks/index.ts b/ui/src/pages/settings/hooks/index.ts index 2797059d..7dbfd881 100644 --- a/ui/src/pages/settings/hooks/index.ts +++ b/ui/src/pages/settings/hooks/index.ts @@ -8,3 +8,4 @@ export { useWebSearchConfig } from './use-websearch-config'; export { useGlobalEnvConfig } from './use-globalenv-config'; export { useProxyConfig } from './use-proxy-config'; export { useRawConfig } from './use-raw-config'; +export { useThinkingConfig } from './use-thinking-config'; diff --git a/ui/src/pages/settings/hooks/use-thinking-config.ts b/ui/src/pages/settings/hooks/use-thinking-config.ts new file mode 100644 index 00000000..89f7bb31 --- /dev/null +++ b/ui/src/pages/settings/hooks/use-thinking-config.ts @@ -0,0 +1,121 @@ +/** + * Thinking Config Hook + * Manages thinking budget configuration with direct API calls + */ + +import { useCallback, useState } from 'react'; +import type { ThinkingConfig, ThinkingMode } from '../types'; + +const DEFAULT_THINKING_CONFIG: ThinkingConfig = { + mode: 'auto', + tier_defaults: { + opus: 'high', + sonnet: 'medium', + haiku: 'low', + }, + show_warnings: true, +}; + +export function useThinkingConfig() { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const fetchConfig = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch('/api/thinking'); + if (!res.ok) throw new Error('Failed to load Thinking config'); + const data = await res.json(); + setConfig(data.config || DEFAULT_THINKING_CONFIG); + } catch (err) { + setError((err as Error).message); + setConfig(DEFAULT_THINKING_CONFIG); + } finally { + setLoading(false); + } + }, []); + + const saveConfig = useCallback( + async (updates: Partial) => { + const currentConfig = config || DEFAULT_THINKING_CONFIG; + const optimisticConfig = { ...currentConfig, ...updates }; + setConfig(optimisticConfig); + + try { + setSaving(true); + setError(null); + + const res = await fetch('/api/thinking', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(optimisticConfig), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Failed to save'); + } + + const data = await res.json(); + setConfig(data.config); + setSuccess(true); + setTimeout(() => setSuccess(false), 1500); + } catch (err) { + setConfig(currentConfig); + setError((err as Error).message); + } finally { + setSaving(false); + } + }, + [config] + ); + + const setMode = useCallback( + (mode: ThinkingMode) => { + saveConfig({ mode }); + }, + [saveConfig] + ); + + const setTierDefault = useCallback( + (tier: 'opus' | 'sonnet' | 'haiku', value: string) => { + const currentDefaults = config?.tier_defaults || DEFAULT_THINKING_CONFIG.tier_defaults; + saveConfig({ + tier_defaults: { ...currentDefaults, [tier]: value }, + }); + }, + [config, saveConfig] + ); + + const setShowWarnings = useCallback( + (show: boolean) => { + saveConfig({ show_warnings: show }); + }, + [saveConfig] + ); + + const setManualOverride = useCallback( + (value: string | number | undefined) => { + saveConfig({ mode: 'manual', override: value }); + }, + [saveConfig] + ); + + return { + config: config || DEFAULT_THINKING_CONFIG, + loading, + saving, + error, + success, + fetchConfig, + saveConfig, + setMode, + setTierDefault, + setShowWarnings, + setManualOverride, + }; +} diff --git a/ui/src/pages/settings/index.tsx b/ui/src/pages/settings/index.tsx index ce86a63c..3068e5fb 100644 --- a/ui/src/pages/settings/index.tsx +++ b/ui/src/pages/settings/index.tsx @@ -17,6 +17,7 @@ import type { SettingsTab } from './types'; // Lazy-loaded sections const WebSearchSection = lazy(() => import('./sections/websearch')); const GlobalEnvSection = lazy(() => import('./sections/globalenv-section')); +const ThinkingSection = lazy(() => import('./sections/thinking')); const ProxySection = lazy(() => import('./sections/proxy')); const AuthSection = lazy(() => import('./sections/auth-section')); @@ -57,6 +58,7 @@ function SettingsPageInner() { }> {activeTab === 'websearch' && } {activeTab === 'globalenv' && } + {activeTab === 'thinking' && } {activeTab === 'proxy' && } {activeTab === 'auth' && } diff --git a/ui/src/pages/settings/sections/thinking/index.tsx b/ui/src/pages/settings/sections/thinking/index.tsx new file mode 100644 index 00000000..fb5d5c17 --- /dev/null +++ b/ui/src/pages/settings/sections/thinking/index.tsx @@ -0,0 +1,208 @@ +/** + * Thinking Section + * Settings section for thinking budget configuration + */ + +import { useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { RefreshCw, CheckCircle2, AlertCircle, Brain } from 'lucide-react'; +import { useThinkingConfig } from '../../hooks'; +import type { ThinkingMode } from '../../types'; + +const THINKING_LEVELS = [ + { value: 'minimal', label: 'Minimal (512 tokens)' }, + { value: 'low', label: 'Low (1K tokens)' }, + { value: 'medium', label: 'Medium (8K tokens)' }, + { value: 'high', label: 'High (24K tokens)' }, + { value: 'xhigh', label: 'Extra High (32K tokens)' }, + { value: 'auto', label: 'Auto (dynamic)' }, +]; + +export default function ThinkingSection() { + const { + config, + loading, + saving, + error, + success, + fetchConfig, + setMode, + setTierDefault, + setShowWarnings, + } = useThinkingConfig(); + + useEffect(() => { + fetchConfig(); + }, [fetchConfig]); + + if (loading) { + return ( +
+
+ + Loading... +
+
+ ); + } + + return ( + <> + {/* Toast-style alerts */} +
+ {error && ( + + + {error} + + )} + {success && ( +
+ + Saved +
+ )} +
+ + {/* Scrollable Content */} + +
+
+ +

+ Configure extended thinking/reasoning for supported models. +

+
+ + {/* Mode Selection */} +
+

Thinking Mode

+
+ {(['auto', 'off', 'manual'] as ThinkingMode[]).map((mode) => ( +
setMode(mode)} + > +
+

{mode}

+

+ {mode === 'auto' && 'Automatically set thinking based on model tier'} + {mode === 'off' && 'Disable extended thinking'} + {mode === 'manual' && 'Use --thinking flag to control manually'} +

+
+
+
+ ))} +
+
+ + {/* Tier Defaults (only shown when mode is auto) */} + {config.mode === 'auto' && ( +
+

Tier Defaults

+

+ Default thinking level for each model tier when in auto mode. +

+ +
+ {(['opus', 'sonnet', 'haiku'] as const).map((tier) => ( +
+ + +
+ ))} +
+
+ )} + + {/* Show Warnings Toggle */} +
+
+

Show Warnings

+

+ Display warnings when thinking values are clamped or adjusted +

+
+ +
+ + {/* Info Box */} +
+

CLI Override

+

+ Use --thinking flag to + override settings per session: +

+
+

ccs gemini --thinking high

+

ccs agy --thinking 16384

+

ccs gemini --thinking off

+
+
+
+ + + {/* Footer */} +
+ +
+ + ); +} diff --git a/ui/src/pages/settings/types.ts b/ui/src/pages/settings/types.ts index 9fdd73de..1dd1ae42 100644 --- a/ui/src/pages/settings/types.ts +++ b/ui/src/pages/settings/types.ts @@ -49,7 +49,25 @@ export interface GlobalEnvConfig { // === Tab Types === -export type SettingsTab = 'websearch' | 'globalenv' | 'proxy' | 'auth'; +export type SettingsTab = 'websearch' | 'globalenv' | 'proxy' | 'auth' | 'thinking'; + +// === Thinking Types === + +export type ThinkingMode = 'auto' | 'off' | 'manual'; + +export interface ThinkingTierDefaults { + opus: string; + sonnet: string; + haiku: string; +} + +export interface ThinkingConfig { + mode: ThinkingMode; + override?: string | number; + tier_defaults: ThinkingTierDefaults; + provider_overrides?: Record>; + show_warnings?: boolean; +} // === Re-exports from api-client ===