@@ -105,18 +105,30 @@ export default function Models({ apiBase }: { apiBase: string }) {
105105 // Combo summary section. null = cold load with no seed (pending strut). Failed reads stay
106106 // null + combosError so an API error never masquerades as "no combos configured".
107107 const combosCacheKey = `ocx.models.combos.v1:${ apiBase } ` ;
108- const [ combos , setCombos ] = useState < ComboItem [ ] | null > ( ( ) => {
108+ const seededCombos = useMemo ( ( ) => {
109109 const own = readSessionListCache < ComboItem [ ] > ( combosCacheKey ) ;
110110 if ( own ) return own ;
111111 // Reuse the Combos workspace session snapshot when Models opens first in the session.
112112 const workspace = readSessionListCache < { combos ?: ComboItem [ ] } > ( `ocx.combos.workspace.v1:${ apiBase } ` ) ;
113113 return Array . isArray ( workspace ?. combos ) ? workspace . combos : null ;
114- } ) ;
115- const [ combosError , setCombosError ] = useState ( false ) ;
114+ } , [ apiBase , combosCacheKey ] ) ;
115+ const combosResource = useDataSurface < ComboItem [ ] > (
116+ `models-combos:${ apiBase } ` ,
117+ [ apiBase ] ,
118+ async ( signal ) => {
119+ const r = await fetch ( `${ apiBase } /api/combos` , { signal } ) ;
120+ const j = await readJsonOrThrow < unknown > ( r ) ;
121+ const next = parseComboList ( j ) ;
122+ writeSessionListCache ( combosCacheKey , next ) ;
123+ return next ;
124+ } ,
125+ { isEmpty : ( ) => false , initialData : seededCombos ?? undefined } ,
126+ ) ;
127+ const combosState = combosResource . state ;
128+ // Keep a previously painted card on a later failure so the catalog does not yank down.
129+ const combos = combosState . data ?? seededCombos ;
130+ const combosError = combosState . showError && combos === null ;
116131 const [ combosOpen , setCombosOpen ] = useState ( readCombosOpen ) ;
117- // True once we have painted a combos card (seed or fetch) so a later fetch failure cannot
118- // unmount it and yank the catalog down.
119- const combosHeldRef = useRef ( combos !== null ) ;
120132
121133 // App owns the in-session view mode; fallback to persisted mode for isolated renders/tests.
122134 const [ selectedProvider , setSelectedProvider ] = useState < string | null > ( null ) ;
@@ -126,28 +138,6 @@ export default function Models({ apiBase }: { apiBase: string }) {
126138 setCombosOpen ( next ) ;
127139 } ;
128140
129- useEffect ( ( ) => {
130- let cancelled = false ;
131- void ( async ( ) => {
132- try {
133- const r = await fetch ( `${ apiBase } /api/combos` ) ;
134- const j = await readJsonOrThrow < unknown > ( r ) ;
135- if ( ! cancelled ) {
136- const next = parseComboList ( j ) ;
137- writeSessionListCache ( combosCacheKey , next ) ;
138- combosHeldRef . current = true ;
139- setCombos ( next ) ;
140- setCombosError ( false ) ;
141- }
142- } catch {
143- if ( ! cancelled && ! combosHeldRef . current ) {
144- setCombosError ( true ) ;
145- }
146- }
147- } ) ( ) ;
148- return ( ) => { cancelled = true ; } ;
149- } , [ apiBase , combosCacheKey ] ) ;
150-
151141 useEffect ( ( ) => ( ) => {
152142 if ( hoverTimerRef . current ) clearTimeout ( hoverTimerRef . current ) ;
153143 } , [ ] ) ;
0 commit comments