@@ -134,6 +134,14 @@ function sortModels(
134134
135135interface ModelListProps {
136136 models : ModelOption [ ] ;
137+ /**
138+ * The authoritative catalog the rows were built from, without any
139+ * synthesized rows for the current selection. Starred state is only
140+ * honored for models present here, so favorited models a provider no
141+ * longer serves stop rendering as starred. Omit to treat every row as
142+ * existing.
143+ */
144+ catalogModels ?: ModelOption [ ] ;
137145 currentModelId : string | null ;
138146 currentModelProviderId : string | null ;
139147 selectedAgentId : string ;
@@ -157,6 +165,7 @@ export const RecommendedModelList = forwardRef<
157165> ( function RecommendedModelList (
158166 {
159167 models,
168+ catalogModels,
160169 currentModelId,
161170 currentModelProviderId,
162171 selectedAgentId,
@@ -166,7 +175,33 @@ export const RecommendedModelList = forwardRef<
166175 } ,
167176 ref ,
168177) {
169- const { isStarred, toggleStar, starredKeys } = useStarredModels ( ) ;
178+ const { toggleStar, starredKeys } = useStarredModels ( ) ;
179+ // Rows include a synthesized entry for the current selection when the
180+ // catalog no longer serves it. Honoring starred state only for catalog
181+ // models keeps a favorited model a provider dropped from rendering as
182+ // starred; the stored entry survives so the star returns if the model does.
183+ const existingModelKeys = useMemo ( ( ) => {
184+ if ( ! catalogModels ) {
185+ return null ;
186+ }
187+ return new Set (
188+ catalogModels . map ( ( model ) =>
189+ modelStarKey ( model . providerId ?? selectedAgentId , model . id ) ,
190+ ) ,
191+ ) ;
192+ } , [ catalogModels , selectedAgentId ] ) ;
193+ const liveStarredKeys = useMemo ( ( ) => {
194+ if ( ! existingModelKeys ) {
195+ return starredKeys ;
196+ }
197+ const live = new Set < string > ( ) ;
198+ for ( const key of starredKeys ) {
199+ if ( existingModelKeys . has ( key ) ) {
200+ live . add ( key ) ;
201+ }
202+ }
203+ return live ;
204+ } , [ existingModelKeys , starredKeys ] ) ;
170205 const [ searchOpen , setSearchOpen ] = useState ( false ) ;
171206 const [ showAll , setShowAll ] = useState ( false ) ;
172207 const [ query , setQuery ] = useState ( "" ) ;
@@ -191,7 +226,7 @@ export const RecommendedModelList = forwardRef<
191226 const recencyMap = useModelRecency ( ) ;
192227 const recommended = useMemo ( ( ) => {
193228 const starred = models . filter ( ( model ) =>
194- starredKeys . has (
229+ liveStarredKeys . has (
195230 modelStarKey ( model . providerId ?? selectedAgentId , model . id ) ,
196231 ) ,
197232 ) ;
@@ -208,7 +243,7 @@ export const RecommendedModelList = forwardRef<
208243 currentModelId ,
209244 currentModelProviderId ,
210245 ) &&
211- ! starredKeys . has (
246+ ! liveStarredKeys . has (
212247 modelStarKey (
213248 entry . model . providerId ?? selectedAgentId ,
214249 entry . model . id ,
@@ -229,7 +264,9 @@ export const RecommendedModelList = forwardRef<
229264 . filter (
230265 ( m ) =>
231266 ! recent . some ( ( r ) => r . id === m . id && r . providerId === m . providerId ) &&
232- ! starredKeys . has ( modelStarKey ( m . providerId ?? selectedAgentId , m . id ) ) ,
267+ ! liveStarredKeys . has (
268+ modelStarKey ( m . providerId ?? selectedAgentId , m . id ) ,
269+ ) ,
233270 ) ;
234271 const shortlist = [ ...recent , ...rec ] ;
235272 if (
@@ -251,7 +288,7 @@ export const RecommendedModelList = forwardRef<
251288 }
252289 const unstarredFallback = models . filter (
253290 ( model ) =>
254- ! starredKeys . has (
291+ ! liveStarredKeys . has (
255292 modelStarKey ( model . providerId ?? selectedAgentId , model . id ) ,
256293 ) ,
257294 ) ;
@@ -265,7 +302,7 @@ export const RecommendedModelList = forwardRef<
265302 currentModelProviderId ,
266303 recencyMap ,
267304 selectedAgentId ,
268- starredKeys ,
305+ liveStarredKeys ,
269306 ] ) ;
270307
271308 useEffect ( ( ) => {
@@ -313,7 +350,7 @@ export const RecommendedModelList = forwardRef<
313350 const unstarred : ModelOption [ ] = [ ] ;
314351 for ( const model of visibleModels ) {
315352 const scopeId = model . providerId ?? selectedAgentId ;
316- ( starredKeys . has ( modelStarKey ( scopeId , model . id ) )
353+ ( liveStarredKeys . has ( modelStarKey ( scopeId , model . id ) )
317354 ? starred
318355 : unstarred
319356 ) . push ( model ) ;
@@ -334,7 +371,7 @@ export const RecommendedModelList = forwardRef<
334371 currentModelProviderId ,
335372 recencyMap ,
336373 selectedAgentId ,
337- starredKeys ,
374+ liveStarredKeys ,
338375 ] ) ;
339376 const sorted = [ ...grouped . starred , ...grouped . unstarred ] ;
340377
@@ -449,15 +486,18 @@ export const RecommendedModelList = forwardRef<
449486 currentModelProviderId ,
450487 ) ;
451488 const scopeId = model . providerId ?? selectedAgentId ;
452- const starred = isStarred ( scopeId , model . id ) ;
489+ const modelKey = modelStarKey ( scopeId , model . id ) ;
490+ const starred = liveStarredKeys . has ( modelKey ) ;
491+ const existsInCatalog =
492+ ! existingModelKeys || existingModelKeys . has ( modelKey ) ;
453493 const showStarredDivider =
454494 index === grouped . starred . length - 1 &&
455495 grouped . unstarred . length > 0 ;
456496 return (
457- < div key = { modelStarKey ( scopeId , model . id ) } >
497+ < div key = { modelKey } >
458498 < div
459499 className = "group flex min-w-0 items-center gap-1"
460- data-model-key = { modelStarKey ( scopeId , model . id ) }
500+ data-model-key = { modelKey }
461501 data-starred = { starred || undefined }
462502 >
463503 < PickerItem
@@ -485,28 +525,30 @@ export const RecommendedModelList = forwardRef<
485525 < IconCheck className = "size-4 shrink-0 text-muted-foreground" />
486526 ) : null }
487527 </ PickerItem >
488- < Button
489- variant = "ghost"
490- size = "icon-xs"
491- selected = { starred }
492- onClick = { ( ) => toggleStar ( scopeId , model . id ) }
493- // Hover-reveal keeps rows calm; keyboard users still
494- // reach the control through row focus
495- // (group-focus-within) or direct focus. The idle
496- // (unstarred) star rests on the ghost icon contract's
497- // muted-foreground — ≈5.7:1 light / ≈6.1:1 dark against
498- // the popover, above the 3:1 WCAG non-text bar
499- // (enforced in globals.test.ts) — and favorited rows
500- // soften to foreground/80 via the selected flag.
501- className = "shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100"
502- aria-label = { t (
503- starred ? "toolbar.unstarModel" : "toolbar.starModel" ,
504- { model : getModelDisplayName ( model ) } ,
505- ) }
506- aria-pressed = { starred }
507- >
508- { starred ? < IconStarFilled /> : < IconStar /> }
509- </ Button >
528+ { existsInCatalog ? (
529+ < Button
530+ variant = "ghost"
531+ size = "icon-xs"
532+ selected = { starred }
533+ onClick = { ( ) => toggleStar ( scopeId , model . id ) }
534+ // Hover-reveal keeps rows calm; keyboard users still
535+ // reach the control through row focus
536+ // (group-focus-within) or direct focus. The idle
537+ // (unstarred) star rests on the ghost icon contract's
538+ // muted-foreground — ≈5.7:1 light / ≈6.1:1 dark against
539+ // the popover, above the 3:1 WCAG non-text bar
540+ // (enforced in globals.test.ts) — and favorited rows
541+ // soften to foreground/80 via the selected flag.
542+ className = "shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100"
543+ aria-label = { t (
544+ starred ? "toolbar.unstarModel" : "toolbar.starModel" ,
545+ { model : getModelDisplayName ( model ) } ,
546+ ) }
547+ aria-pressed = { starred }
548+ >
549+ { starred ? < IconStarFilled /> : < IconStar /> }
550+ </ Button >
551+ ) : null }
510552 </ div >
511553 { showStarredDivider ? (
512554 < Separator
0 commit comments