@@ -84,6 +84,16 @@ import { cn } from "@/lib/utils";
8484
8585type SettingsTab = "general" | "models" | "integrations" | "chat" | "voice" | "automation" | "data" ;
8686
87+ const SETTINGS_SEARCH_ITEMS : { tab : SettingsTab ; label : string ; keywords : string } [ ] = [
88+ { tab : "general" , label : "Appearance, density & motion" , keywords : "theme color compact comfortable animation reduced motion language server gpu cache" } ,
89+ { tab : "models" , label : "Models & hardware" , keywords : "ollama hugging face download vram recommendation gguf" } ,
90+ { tab : "integrations" , label : "Accounts, providers & MCP" , keywords : "github hugging face api key custom gpu backend figma mcp" } ,
91+ { tab : "chat" , label : "Chat & agent behavior" , keywords : "prompt temperature context tokens agent steps tool calls" } ,
92+ { tab : "voice" , label : "Voice & speech" , keywords : "microphone transcription tts read aloud voice" } ,
93+ { tab : "automation" , label : "Automation" , keywords : "scheduled task interval prompt" } ,
94+ { tab : "data" , label : "Data, activity & diagnostics" , keywords : "export import logs memory activity clear" } ,
95+ ] ;
96+
8797// Ollama pulls Hugging Face GGUF models via a "hf.co/user/repo[:quant]" model
8898// name — accept a pasted full URL or the "huggingface.co/" host too, rather
8999// than making the user hand-edit what they copied from their browser.
@@ -132,6 +142,7 @@ export default function Settings() {
132142 const [ hasApi , setHasApi ] = useState ( true ) ;
133143 const [ search , setSearch ] = useState ( "" ) ;
134144 const [ activeTab , setActiveTab ] = useState < SettingsTab > ( "general" ) ;
145+ const [ settingsQuery , setSettingsQuery ] = useState ( "" ) ;
135146 const [ openaiKeyInput , setOpenaiKeyInput ] = useState ( "" ) ;
136147 const [ anthropicKeyInput , setAnthropicKeyInput ] = useState ( "" ) ;
137148 const [ openaiKeySet , setOpenaiKeySet ] = useState ( false ) ;
@@ -638,6 +649,9 @@ export default function Settings() {
638649 const merged = { ...settings , ...partial } ;
639650 setSettings ( merged ) ;
640651 await window . api . settings . save ( partial ) ;
652+ if ( partial . uiDensity !== undefined || partial . reduceMotion !== undefined ) {
653+ window . dispatchEvent ( new CustomEvent ( "app:display-settings" , { detail : merged } ) ) ;
654+ }
641655 }
642656
643657 async function addMlxModel ( ) {
@@ -843,11 +857,23 @@ export default function Settings() {
843857 return (
844858 < ScrollArea className = "h-full bg-background/25" >
845859 < div className = "mx-auto max-w-5xl px-4 pb-20 pt-16 sm:px-6 md:pt-8 2xl:max-w-6xl" >
846- < div className = "mb-8 flex items-center gap-3" >
847- < span className = "flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary shadow-sm" > < Settings2 className = "size-5" /> </ span >
848- < div > < h1 className = "text-2xl font-semibold tracking-tight" > { t . settings } </ h1 > < p className = "mt-0.5 text-xs text-muted-foreground" > Configure models, integrations, automation, and your workspace.</ p > </ div >
860+ < div className = "mb-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-center" >
861+ < div className = "flex items-center gap-3" > < span className = "flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary shadow-sm" > < Settings2 className = "size-5" /> </ span >
862+ < div > < h1 className = "text-2xl font-semibold tracking-tight" > { t . settings } </ h1 > < p className = "mt-0.5 text-xs text-muted-foreground" > Configure models, integrations, automation, and your workspace.</ p > </ div > </ div >
863+ < div className = "relative w-full sm:w-72" >
864+ < Search className = "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
865+ < Input value = { settingsQuery } onChange = { ( e ) => setSettingsQuery ( e . target . value ) } placeholder = "Search settings…" className = "h-10 rounded-xl bg-card pl-9 shadow-sm" aria-label = "Search settings" />
866+ </ div >
849867 </ div >
850868
869+ { settingsQuery . trim ( ) && (
870+ < div className = "surface-glass shadow-soft mb-6 grid gap-2 rounded-2xl border border-border/70 p-3 sm:grid-cols-2" >
871+ { SETTINGS_SEARCH_ITEMS . filter ( ( item ) => `${ item . label } ${ item . keywords } ` . toLowerCase ( ) . includes ( settingsQuery . trim ( ) . toLowerCase ( ) ) ) . map ( ( item ) => (
872+ < button key = { item . tab } onClick = { ( ) => { setActiveTab ( item . tab ) ; setSettingsQuery ( "" ) ; } } className = "rounded-xl border border-transparent p-3 text-left text-sm font-medium transition-colors hover:border-primary/20 hover:bg-primary/5" > { item . label } < span className = "mt-0.5 block text-xs font-normal text-muted-foreground" > Open { item . tab } settings</ span > </ button >
873+ ) ) }
874+ </ div >
875+ ) }
876+
851877 < Tabs
852878 value = { activeTab }
853879 onValueChange = { ( v ) => setActiveTab ( v as SettingsTab ) }
@@ -990,6 +1016,17 @@ export default function Settings() {
9901016 ) ;
9911017 } ) ( ) }
9921018 </ SettingsRow >
1019+ < SettingsRow label = "Warm model cache" description = "Maximum llama.cpp model variants retained in RAM/VRAM. Lower values save memory; higher values make model switching faster." >
1020+ < Select value = { String ( settings . llamaCppMaxCachedModels ?? 2 ) } onValueChange = { ( v ) => saveSettings ( { llamaCppMaxCachedModels : Number ( v ) } ) } >
1021+ < SelectTrigger size = "sm" className = "w-44" > < SelectValue /> </ SelectTrigger >
1022+ < SelectContent >
1023+ < SelectItem value = "1" > 1 model · minimum memory</ SelectItem >
1024+ < SelectItem value = "2" > 2 models · balanced</ SelectItem >
1025+ < SelectItem value = "3" > 3 models · faster switching</ SelectItem >
1026+ < SelectItem value = "4" > 4 models · workstation</ SelectItem >
1027+ </ SelectContent >
1028+ </ Select >
1029+ </ SettingsRow >
9931030 < SettingsRow label = { t . modelsDir } stacked >
9941031 < div className = "flex flex-wrap items-center gap-2" >
9951032 < span className = "truncate rounded border border-border bg-muted px-2 py-1 font-mono text-xs" >
@@ -1116,6 +1153,17 @@ export default function Settings() {
11161153 ) ) }
11171154 </ div >
11181155 </ SettingsRow >
1156+ < SettingsRow label = "Interface density" description = "Choose how much information fits on screen." >
1157+ < Select value = { settings ?. uiDensity ?? "comfortable" } onValueChange = { ( v ) => saveSettings ( { uiDensity : v as "comfortable" | "compact" } ) } >
1158+ < SelectTrigger size = "sm" className = "w-40" > < SelectValue /> </ SelectTrigger >
1159+ < SelectContent > < SelectItem value = "comfortable" > Comfortable</ SelectItem > < SelectItem value = "compact" > Compact</ SelectItem > </ SelectContent >
1160+ </ Select >
1161+ </ SettingsRow >
1162+ < SettingsRow label = "Reduce motion" description = "Minimize animations and smooth transitions for accessibility." >
1163+ < button type = "button" role = "switch" aria-checked = { settings ?. reduceMotion ?? false } onClick = { ( ) => saveSettings ( { reduceMotion : ! ( settings ?. reduceMotion ?? false ) } ) } className = { cn ( "relative h-6 w-11 rounded-full transition-colors" , settings ?. reduceMotion ? "bg-primary" : "bg-muted" ) } >
1164+ < span className = { cn ( "absolute top-0.5 size-5 rounded-full bg-white shadow-sm transition-transform" , settings ?. reduceMotion ? "translate-x-5" : "translate-x-0.5" ) } />
1165+ </ button >
1166+ </ SettingsRow >
11191167 </ SettingsSection >
11201168
11211169 < SettingsSection title = { t . language } className = "mt-8" >
@@ -2196,6 +2244,25 @@ export default function Settings() {
21962244 </ SettingsRow >
21972245 </ SettingsSection >
21982246
2247+ < SettingsSection title = "Agent runtime" description = "Control how independently Agent Mode can work before returning control to you." className = "mt-8" >
2248+ < SettingsRow label = "Maximum tool steps per turn" description = "Stops runaway tool loops. Larger values suit long repository analysis; smaller values provide more frequent checkpoints." >
2249+ < Input type = "number" min = { 5 } max = { 100 } step = { 5 } value = { settings . agentMaxSteps ?? 25 } onChange = { ( e ) => saveSettings ( { agentMaxSteps : Math . max ( 5 , Math . min ( Number ( e . target . value ) , 100 ) ) } ) } className = "w-24" aria-label = "Maximum agent tool steps" />
2250+ </ SettingsRow >
2251+ < SettingsRow label = "Recommended profile" stacked >
2252+ < div className = "grid gap-2 sm:grid-cols-3" >
2253+ { [
2254+ { label : "Cautious" , value : 10 , note : "Frequent user review" } ,
2255+ { label : "Balanced" , value : 25 , note : "Best default" } ,
2256+ { label : "Autonomous" , value : 50 , note : "Long coding tasks" } ,
2257+ ] . map ( ( profile ) => (
2258+ < button key = { profile . label } onClick = { ( ) => saveSettings ( { agentMaxSteps : profile . value } ) } className = { cn ( "rounded-xl border p-3 text-left transition-colors" , ( settings . agentMaxSteps ?? 25 ) === profile . value ? "border-primary/40 bg-primary/5" : "border-border hover:bg-muted/50" ) } >
2259+ < span className = "block text-sm font-medium" > { profile . label } </ span > < span className = "mt-0.5 block text-xs text-muted-foreground" > { profile . value } steps · { profile . note } </ span >
2260+ </ button >
2261+ ) ) }
2262+ </ div >
2263+ </ SettingsRow >
2264+ </ SettingsSection >
2265+
21992266 < SettingsSection
22002267 title = { t . promptLibrary }
22012268 description = { t . promptLibraryVariablesHint }
0 commit comments