feat: add verified desktop ChatGPT subscription connection
This commit is contained in:
+30
-40
@@ -8,7 +8,7 @@ import { ModelMenuList, ThinkingMenuList } from "@/components/settings/model-men
|
||||
import { composeSlotModel } from "@/components/settings/agent-section";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
import { notifySettingsSaved, useSettingsSaved } from "@/components/settings-signal";
|
||||
import { notifySettingsSaved, onSettingsSaved, useSettingsSaved } from "@/components/settings-signal";
|
||||
import { defaultOrderLevel, populateLevel, THINKING_LABEL } from "@/components/thinking-default";
|
||||
|
||||
/** GET /api/models is cached per page load for instant menu opens, but
|
||||
@@ -17,51 +17,40 @@ import { defaultOrderLevel, populateLevel, THINKING_LABEL } from "@/components/t
|
||||
* every model-menu open (stale-while-revalidate: the cached list shows
|
||||
* instantly, the fresh one swaps in while the menu is open). */
|
||||
let catalogCache: ModelCatalogResponse | undefined;
|
||||
let catalogInflight: Promise<ModelCatalogResponse> | undefined;
|
||||
|
||||
function fetchCatalog(force = false): Promise<ModelCatalogResponse> {
|
||||
if (force) {
|
||||
catalogCache = undefined;
|
||||
catalogInflight = undefined;
|
||||
}
|
||||
catalogInflight ??= fetch("/api/models")
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`catalog request failed: ${r.status}`);
|
||||
return r.json() as Promise<ModelCatalogResponse>;
|
||||
})
|
||||
.then((c) => {
|
||||
catalogCache = c;
|
||||
return c;
|
||||
})
|
||||
.catch((err) => {
|
||||
catalogInflight = undefined; // retry on next mount/open
|
||||
throw err;
|
||||
});
|
||||
return catalogInflight;
|
||||
let catalogInflight: Promise<void> | undefined;
|
||||
let catalogGeneration = 0;
|
||||
let stopCatalogSignals: (() => void) | undefined;
|
||||
const catalogReaders = new Set<() => void>();
|
||||
const publishCatalog = () => { for (const reader of catalogReaders) reader(); };
|
||||
function fetchCatalog(revalidate = false): void {
|
||||
if (revalidate) { catalogGeneration++; catalogInflight = undefined; }
|
||||
if (catalogInflight) return;
|
||||
const generation = catalogGeneration;
|
||||
catalogInflight = fetch("/api/models")
|
||||
.then((r) => { if (!r.ok) throw new Error(`catalog request failed: ${r.status}`); return r.json() as Promise<ModelCatalogResponse>; })
|
||||
.then((c) => { if (generation === catalogGeneration) { catalogCache = c; publishCatalog(); } })
|
||||
.catch(() => { /* existing unavailable/usage-only posture, no retry timer */ })
|
||||
.finally(() => { if (generation === catalogGeneration) catalogInflight = undefined; });
|
||||
}
|
||||
|
||||
export function useModelCatalog(): { catalog: ModelCatalogResponse | undefined; refresh: () => void } {
|
||||
const [catalog, setCatalog] = useState(catalogCache);
|
||||
useEffect(() => {
|
||||
if (catalogCache) return;
|
||||
let alive = true;
|
||||
fetchCatalog()
|
||||
.then((c) => alive && setCatalog(c))
|
||||
.catch(() => {
|
||||
/* the picker stays closed on failure; the buttons still
|
||||
display the strong slot from the record fetch */
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
const refresh = useCallback(() => {
|
||||
fetchCatalog(true)
|
||||
.then(setCatalog)
|
||||
.catch(() => {
|
||||
/* refresh is best-effort: the cached list stays on failure */
|
||||
const first = catalogReaders.size === 0;
|
||||
const read = () => setCatalog(catalogCache);
|
||||
catalogReaders.add(read);
|
||||
if (first) {
|
||||
stopCatalogSignals = onSettingsSaved((section) => {
|
||||
if (section === "keys") {
|
||||
catalogGeneration++; catalogInflight = undefined; catalogCache = undefined;
|
||||
publishCatalog(); fetchCatalog();
|
||||
} else if (section === "models") fetchCatalog(true);
|
||||
});
|
||||
fetchCatalog(true);
|
||||
} else fetchCatalog();
|
||||
read();
|
||||
return () => { catalogReaders.delete(read); if (!catalogReaders.size) { stopCatalogSignals?.(); stopCatalogSignals = undefined; } };
|
||||
}, []);
|
||||
const refresh = useCallback(() => fetchCatalog(true), []);
|
||||
return { catalog, refresh };
|
||||
}
|
||||
|
||||
@@ -322,6 +311,7 @@ export function ModelControl({ chatId }: { chatId?: string }) {
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative flex items-center gap-1.5" data-testid="model-control">
|
||||
{effectiveProvider === "chatgpt" && <a href="https://chatgpt.com/settings/usage" target="_blank" rel="noreferrer" className="text-xs text-muted-foreground underline-offset-2 hover:underline">Using ChatGPT plan</a>}
|
||||
<Hint
|
||||
// Trimmed (owner, subtlety round 2026-09-25): "Model selector"
|
||||
// — the long sentence was way too much. ⌘K opens this same
|
||||
|
||||
Reference in New Issue
Block a user