Skip to content

Commit 8d4122a

Browse files
committed
fix(gui): load Models combos through the shared resource layer
Removes the effect-based /api/combos fetch that React Doctor blocked (setState after await) and keeps the seed/error card contract via useDataSurface.
1 parent 9f82c64 commit 8d4122a

1 file changed

Lines changed: 18 additions & 28 deletions

File tree

gui/src/pages/Models.tsx

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)