1- import React , { useState , useCallback } from "react" ;
1+ import React , { useState , useCallback , useEffect , useRef } from "react" ;
22import { Plus , Loader2 } from "lucide-react" ;
33import { SUPPORTED_PROVIDERS , PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers" ;
44import { KNOWN_MODELS } from "@/common/constants/knownModels" ;
@@ -15,6 +15,10 @@ import {
1515 SelectValue ,
1616} from "@/browser/components/ui/select" ;
1717import { Button } from "@/browser/components/ui/button" ;
18+ import { ModelSelector } from "@/browser/components/ModelSelector" ;
19+ import { MODE_AI_DEFAULTS_KEY } from "@/common/constants/storage" ;
20+ import { normalizeModeAiDefaults , type ModeAiDefaults } from "@/common/types/modeAiDefaults" ;
21+ import { updatePersistedState } from "@/browser/hooks/usePersistedState" ;
1822
1923// Providers to exclude from the custom models UI (handled specially or internal)
2024const HIDDEN_PROVIDERS = new Set ( [ "mux-gateway" ] ) ;
@@ -37,13 +41,97 @@ export function ModelsSection() {
3741 const [ editing , setEditing ] = useState < EditingState | null > ( null ) ;
3842 const [ error , setError ] = useState < string | null > ( null ) ;
3943
44+ // Compaction model state
45+ const [ compactionModel , setCompactionModelState ] = useState < string > ( "" ) ;
46+ const [ compactionLoaded , setCompactionLoaded ] = useState ( false ) ;
47+ const compactionSaveTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
48+
4049 const selectableProviders = SUPPORTED_PROVIDERS . filter (
4150 ( provider ) => ! HIDDEN_PROVIDERS . has ( provider )
4251 ) ;
43- const { defaultModel, setDefaultModel, hiddenModels, hideModel, unhideModel } =
52+ const { models , defaultModel, setDefaultModel, hiddenModels, hideModel, unhideModel } =
4453 useModelsFromSettings ( ) ;
4554 const gateway = useGateway ( ) ;
4655
56+ // Load compaction model from config on mount
57+ useEffect ( ( ) => {
58+ if ( ! api ) return ;
59+
60+ void api . config
61+ . getConfig ( )
62+ . then ( ( cfg ) => {
63+ const normalized = normalizeModeAiDefaults ( cfg . modeAiDefaults ?? { } ) ;
64+ setCompactionModelState ( normalized . compact ?. modelString ?? "" ) ;
65+ setCompactionLoaded ( true ) ;
66+ } )
67+ . catch ( ( ) => {
68+ setCompactionLoaded ( true ) ;
69+ } ) ;
70+ } , [ api ] ) ;
71+
72+ // Debounced save for compaction model changes
73+ const setCompactionModel = useCallback (
74+ ( model : string ) => {
75+ setCompactionModelState ( model ) ;
76+
77+ // Clear any pending save
78+ if ( compactionSaveTimerRef . current ) {
79+ clearTimeout ( compactionSaveTimerRef . current ) ;
80+ }
81+
82+ compactionSaveTimerRef . current = setTimeout ( ( ) => {
83+ if ( ! api ) return ;
84+
85+ // Update local cache immediately for non-React readers
86+ updatePersistedState < ModeAiDefaults > (
87+ MODE_AI_DEFAULTS_KEY ,
88+ ( prev ) => {
89+ const next = { ...prev } ;
90+ if ( ! model ) {
91+ if ( next . compact ) {
92+ delete next . compact . modelString ;
93+ if ( ! next . compact . thinkingLevel ) delete next . compact ;
94+ }
95+ } else {
96+ next . compact = { ...next . compact , modelString : model } ;
97+ }
98+ return next ;
99+ } ,
100+ { }
101+ ) ;
102+
103+ // Persist to backend
104+ void api . config . getConfig ( ) . then ( ( cfg ) => {
105+ const existing = normalizeModeAiDefaults ( cfg . modeAiDefaults ?? { } ) ;
106+ const updated : ModeAiDefaults = { ...existing } ;
107+
108+ if ( ! model ) {
109+ if ( updated . compact ) {
110+ delete updated . compact . modelString ;
111+ if ( ! updated . compact . thinkingLevel ) {
112+ delete updated . compact ;
113+ }
114+ }
115+ } else {
116+ updated . compact = { ...updated . compact , modelString : model } ;
117+ }
118+
119+ void api . config . updateModeAiDefaults ( { modeAiDefaults : updated } ) ;
120+ } ) ;
121+ } , 400 ) ;
122+ } ,
123+ [ api ]
124+ ) ;
125+
126+ // Cleanup save timer on unmount
127+ useEffect ( ( ) => {
128+ return ( ) => {
129+ if ( compactionSaveTimerRef . current ) {
130+ clearTimeout ( compactionSaveTimerRef . current ) ;
131+ }
132+ } ;
133+ } , [ ] ) ;
134+
47135 // Check if a model already exists (for duplicate prevention)
48136 const modelExists = useCallback (
49137 ( provider : string , modelId : string , excludeOriginal ?: string ) : boolean => {
@@ -169,15 +257,47 @@ export function ModelsSection() {
169257 modelId : model . providerModelId ,
170258 fullId : model . id ,
171259 aliases : model . aliases ,
260+ contextWindow : model . contextWindow ,
261+ description : model . description ,
172262 } ) ) ;
173263
174264 const customModels = getCustomModels ( ) ;
175265
176266 return (
177267 < div className = "space-y-4" >
178- < p className = "text-muted text-xs" >
179- Manage your models. Click the star to set a default model for new workspaces.
180- </ p >
268+ { /* Model Defaults */ }
269+ { compactionLoaded && (
270+ < div className = "border-border-medium bg-background-secondary rounded-md border p-3" >
271+ < div className = "text-foreground mb-3 text-sm font-medium" > Model Defaults</ div >
272+
273+ { /* Default Model */ }
274+ < div className = "mb-4 space-y-1" >
275+ < div className = "text-muted text-xs" > Default Model</ div >
276+ < ModelSelector
277+ value = { defaultModel }
278+ onChange = { setDefaultModel }
279+ models = { models }
280+ hiddenModels = { hiddenModels }
281+ />
282+ < div className = "text-muted-light text-[10px]" > Used for new workspaces</ div >
283+ </ div >
284+
285+ { /* Compaction Model */ }
286+ < div className = "space-y-1" >
287+ < div className = "text-muted text-xs" > Compaction Model</ div >
288+ < ModelSelector
289+ value = { compactionModel }
290+ emptyLabel = "Use workspace model"
291+ onChange = { setCompactionModel }
292+ models = { models }
293+ hiddenModels = { hiddenModels }
294+ />
295+ < div className = "text-muted-light text-[10px]" >
296+ Model used for compacting history. Falls back to workspace model if not set.
297+ </ div >
298+ </ div >
299+ </ div >
300+ ) }
181301
182302 { /* Custom Models - shown first */ }
183303 < div className = "space-y-1.5" >
@@ -236,14 +356,12 @@ export function ModelsSection() {
236356 modelId = { model . modelId }
237357 fullId = { model . fullId }
238358 isCustom = { true }
239- isDefault = { defaultModel === model . fullId }
240359 isEditing = { isModelEditing }
241360 editValue = { isModelEditing ? editing . newModelId : undefined }
242361 editError = { isModelEditing ? error : undefined }
243362 saving = { false }
244363 hasActiveEdit = { editing !== null }
245364 isGatewayEnabled = { gateway . modelUsesGateway ( model . fullId ) }
246- onSetDefault = { ( ) => setDefaultModel ( model . fullId ) }
247365 onStartEdit = { ( ) => handleStartEdit ( model . provider , model . modelId ) }
248366 onSaveEdit = { handleSaveEdit }
249367 onCancelEdit = { handleCancelEdit }
@@ -279,11 +397,11 @@ export function ModelsSection() {
279397 modelId = { model . modelId }
280398 fullId = { model . fullId }
281399 aliases = { model . aliases }
400+ contextWindow = { model . contextWindow }
401+ description = { model . description }
282402 isCustom = { false }
283- isDefault = { defaultModel === model . fullId }
284403 isEditing = { false }
285404 isGatewayEnabled = { gateway . modelUsesGateway ( model . fullId ) }
286- onSetDefault = { ( ) => setDefaultModel ( model . fullId ) }
287405 isHiddenFromSelector = { hiddenModels . includes ( model . fullId ) }
288406 onToggleVisibility = { ( ) =>
289407 hiddenModels . includes ( model . fullId )
0 commit comments