Gate the model picker behind credentials: fetch on Get models or after OAuth login

This commit is contained in:
ZacharyZhang-NY committed 2026-05-30 01:05:14 -04:00
1 parent ddd8663ecc
commit d4ace283d1
3 files changed
+37 -25

No files matched your search

@@ -60,7 +60,7 @@ export function Onboarding({
providerKey: firstProvider.key, providerKey: firstProvider.key,
providerSecret: "", providerSecret: "",
providerBaseUrl: "", providerBaseUrl: "",
providerModel: firstProvider.defaultModel ?? firstProvider.models[0]?.id ?? "", providerModel: "",
goalTitle: "", goalTitle: "",
goalDescription: "", goalDescription: "",
}); });
@@ -159,7 +159,7 @@ export function Onboarding({
<div className="row" style={{ gap: 10 }}> <div className="row" style={{ gap: 10 }}>
<Icon name="bot" size={16} /> <Icon name="bot" size={16} />
<strong style={{ fontSize: 13 }}>{selectedProvider.label}</strong> <strong style={{ fontSize: 13 }}>{selectedProvider.label}</strong>
<Pill tone="violet">{draft.providerModel}</Pill> {draft.providerModel && <Pill tone="violet">{draft.providerModel}</Pill>}
</div> </div>
<div style={{ marginTop: 10, fontSize: 12, color: "var(--text-3)" }}> <div style={{ marginTop: 10, fontSize: 12, color: "var(--text-3)" }}>
We never embed the key in the build. Credentials encrypt locally via OS-protected storage. We never embed the key in the build. Credentials encrypt locally via OS-protected storage.
@@ -264,7 +264,7 @@ function ProviderStep({
className={`option-card ${draft.providerKey === provider.key ? "active" : ""}`.trim()} className={`option-card ${draft.providerKey === provider.key ? "active" : ""}`.trim()}
onClick={() => onChange({ onClick={() => onChange({
providerKey: provider.key, providerKey: provider.key,
providerModel: provider.defaultModel ?? provider.models[0]?.id ?? "", providerModel: "",
providerSecret: "", providerSecret: "",
providerBaseUrl: "", providerBaseUrl: "",
})} })}
@@ -301,8 +301,11 @@ function ProviderCredentialFields({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null); const [loadError, setLoadError] = useState<string | null>(null);
const models = fetched ?? selected.models; const hasCredential = isOAuthLike
const canLoad = isCustom ? Boolean(draft.providerBaseUrl.trim() && draft.providerSecret.trim()) : Boolean(draft.providerSecret.trim()); ? Boolean(draft.providerSecret.trim())
: isCustom
? Boolean(draft.providerBaseUrl.trim() && draft.providerSecret.trim())
: Boolean(draft.providerSecret.trim());
const loadModels = async (secret: string) => { const loadModels = async (secret: string) => {
setLoading(true); setLoading(true);
@@ -347,7 +350,7 @@ function ProviderCredentialFields({
className="input" className="input"
value={draft.providerBaseUrl} value={draft.providerBaseUrl}
placeholder={selected.customEndpoint === "openai" ? "https://host/v1" : "https://host"} placeholder={selected.customEndpoint === "openai" ? "https://host/v1" : "https://host"}
onChange={(event) => onChange({ providerBaseUrl: event.target.value })} onChange={(event) => onChange({ providerBaseUrl: event.target.value, providerModel: "" })}
/> />
</Field> </Field>
<Field label="API key"> <Field label="API key">
@@ -356,7 +359,7 @@ function ProviderCredentialFields({
type="password" type="password"
autoComplete="off" autoComplete="off"
value={draft.providerSecret} value={draft.providerSecret}
onChange={(event) => onChange({ providerSecret: event.target.value })} onChange={(event) => onChange({ providerSecret: event.target.value, providerModel: "" })}
/> />
</Field> </Field>
</> </>
@@ -367,31 +370,38 @@ function ProviderCredentialFields({
type="password" type="password"
autoComplete="off" autoComplete="off"
value={draft.providerSecret} value={draft.providerSecret}
onChange={(event) => onChange({ providerSecret: event.target.value })} onChange={(event) => onChange({ providerSecret: event.target.value, providerModel: "" })}
/> />
</Field> </Field>
)} )}
{!isOAuthLike && ( {!isOAuthLike && (
<Button tone="ghost" onClick={loadTypedModels} disabled={!canLoad || loading}> <Button tone="ghost" onClick={loadTypedModels} disabled={!hasCredential || loading}>
{loading ? "Loading models…" : fetched ? "Reload models" : "Load models"} {loading ? "Getting models…" : fetched ? "Reload models" : "Get models"}
</Button> </Button>
)} )}
{loadError && <div style={{ color: "var(--red)", fontSize: 12 }}>{loadError}</div>} {loadError && <div style={{ color: "var(--red)", fontSize: 12 }}>{loadError}</div>}
<Field label="Model"> {hasCredential && (
{models.length ? ( <Field label="Model">
<select {fetched ? (
className="input" <select
value={draft.providerModel} className="input"
onChange={(event) => onChange({ providerModel: event.target.value })} value={draft.providerModel}
> onChange={(event) => onChange({ providerModel: event.target.value })}
{models.map((model) => ( >
<option key={model.id} value={model.id}>{model.name}</option> {fetched.map((model) => (
))} <option key={model.id} value={model.id}>{model.name}</option>
</select> ))}
) : ( </select>
<input className="input" value={draft.providerModel} onChange={(event) => onChange({ providerModel: event.target.value })} /> ) : (
)} <input
</Field> className="input"
value={draft.providerModel}
placeholder={isOAuthLike ? "Loading…" : "Click “Get models”, or type a model id"}
onChange={(event) => onChange({ providerModel: event.target.value })}
/>
)}
</Field>
)}
</div> </div>
); );
} }
@@ -22,6 +22,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
await page.getByRole("button", { name: "Get started" }).click(); await page.getByRole("button", { name: "Get started" }).click();
await page.getByLabel("API key").fill("e2e-secret"); await page.getByLabel("API key").fill("e2e-secret");
await page.getByLabel("Model").fill("gpt-5.4");
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Company name").fill("Acme Operations"); await page.getByLabel("Company name").fill("Acme Operations");
await page.getByLabel("Your name").fill("Dana Lee"); await page.getByLabel("Your name").fill("Dana Lee");
@@ -21,6 +21,7 @@ test("captures every page for visual review", async () => {
await page.getByRole("button", { name: "Get started" }).click(); await page.getByRole("button", { name: "Get started" }).click();
await page.screenshot({ path: join(outDir, "01-onboarding-provider.png"), fullPage: false }); await page.screenshot({ path: join(outDir, "01-onboarding-provider.png"), fullPage: false });
await page.getByLabel("API key").fill("local-dev-key"); await page.getByLabel("API key").fill("local-dev-key");
await page.getByLabel("Model").fill("gpt-5.4");
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Company name").fill("OrbitOps"); await page.getByLabel("Company name").fill("OrbitOps");