From e7eb829b48cea8267c8a640c5664ed620913b159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 21 May 2026 22:15:15 -0400 Subject: [PATCH] Expose OAuth and account providers in onboarding The provider step was filtered to API_KEY only, which hid GitHub Copilot, Google Antigravity, and GPT account from the founding-CEO runtime picker even though they are first-class providers in the catalog. Show every non-cloud provider, render an "Access token" field in place of the API-key field for OAuth and account auths, and surface a button that opens the provider's official sign-in page via the new openExternalUrl IPC (validated to http(s) only). OnboardingService.resolveProvider now accepts any non-cloud auth type. Cloud providers still route through Settings since their multi-field config does not fit the linear onboarding step. --- AgentCompany-New/src/main/app/ipc-handlers.ts | 14 +++- .../src/main/services/onboarding-service.ts | 4 +- AgentCompany-New/src/preload/index.ts | 1 + .../src/renderer/src/app/Onboarding.tsx | 71 +++++++++++++------ AgentCompany-New/src/shared/desktop-api.ts | 1 + AgentCompany-New/src/shared/ipc.ts | 1 + 6 files changed, 69 insertions(+), 23 deletions(-) diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 93c2ca5e..c7101754 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -1,4 +1,4 @@ -import { ipcMain, type BrowserWindow } from "electron"; +import { ipcMain, shell, type BrowserWindow } from "electron"; import type { DatabaseConnection } from "../database/connection"; import { IPC_CHANNELS } from "../../shared/ipc"; import { fail, ok, type DesktopResult } from "../../shared/result"; @@ -86,6 +86,18 @@ export function registerIpcHandlers(deps: IpcDeps): void { return fail("provider_sign_in_failed", error instanceof Error ? error.message : String(error)); } }); + ipcMain.handle(IPC_CHANNELS.openExternalUrl, async (_event, url: string) => { + try { + const parsed = new URL(url); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return fail("external_url_blocked", "Only http(s) URLs may be opened externally."); + } + await shell.openExternal(parsed.toString()); + return ok(null); + } catch (error) { + return fail("external_url_failed", error instanceof Error ? error.message : String(error)); + } + }); ipcMain.handle(IPC_CHANNELS.createProviderResponse, async (_event, input) => { try { await providers.createResponse(input); diff --git a/AgentCompany-New/src/main/services/onboarding-service.ts b/AgentCompany-New/src/main/services/onboarding-service.ts index 88d66519..151cd90e 100644 --- a/AgentCompany-New/src/main/services/onboarding-service.ts +++ b/AgentCompany-New/src/main/services/onboarding-service.ts @@ -85,8 +85,8 @@ export class OnboardingService { private resolveProvider(providerKey: string) { const provider = PROVIDER_CATALOG.find((entry) => entry.key === providerKey); if (!provider) throw new Error(`Provider ${providerKey} is unavailable.`); - if (provider.authType !== PROVIDER_AUTH_TYPE.API_KEY) { - throw new Error("Onboarding requires an API key provider."); + if (provider.authType === PROVIDER_AUTH_TYPE.CLOUD) { + throw new Error("Cloud providers need extended configuration — connect them from Settings after the company is created."); } return provider; } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 4bc996bf..ebb155cd 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -22,6 +22,7 @@ const api: DesktopApi = { markChatSpaceRead: (input) => invoke(IPC_CHANNELS.markChatSpaceRead, input), saveProviderCredential: (input) => invoke(IPC_CHANNELS.saveProviderCredential, input), launchProviderSignIn: (input) => invoke(IPC_CHANNELS.launchProviderSignIn, input), + openExternalUrl: (url) => invoke(IPC_CHANNELS.openExternalUrl, url), createProviderResponse: (input) => invoke(IPC_CHANNELS.createProviderResponse, input), importSkillDirectory: (input) => invoke(IPC_CHANNELS.importSkillDirectory, input), bindSkill: (input) => invoke(IPC_CHANNELS.bindSkill, input), diff --git a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx index 58a77915..860b4d2c 100644 --- a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx +++ b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx @@ -41,13 +41,12 @@ export function Onboarding({ onCancel: (() => void) | null; onCompleted: () => void; }) { - const apiProviders = useMemo( - () => snapshot.providerCatalog.filter((entry) => entry.authType === PROVIDER_AUTH_TYPE.API_KEY), + const availableProviders = useMemo( + () => snapshot.providerCatalog.filter((entry) => entry.authType !== PROVIDER_AUTH_TYPE.CLOUD), [snapshot.providerCatalog], ); - const firstProvider = apiProviders[0]; - if (!firstProvider) throw new Error("Provider catalog must include at least one API-key provider."); - if (!firstProvider.defaultModel) throw new Error("Default API provider needs a default model."); + const firstProvider = availableProviders[0]; + if (!firstProvider) throw new Error("Provider catalog must include at least one non-cloud provider."); const [step, setStep] = useState("welcome"); const [draft, setDraft] = useState({ @@ -58,14 +57,14 @@ export function Onboarding({ executionMode: "ai_ceo", providerKey: firstProvider.key, providerSecret: "", - providerModel: firstProvider.defaultModel, + providerModel: firstProvider.defaultModel ?? firstProvider.models[0]?.id ?? "", goalTitle: "", goalDescription: "", }); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); - const selectedProvider = requireProvider(apiProviders, draft.providerKey); + const selectedProvider = requireProvider(availableProviders, draft.providerKey); const update = (patch: Partial) => setDraft((current) => ({ ...current, ...patch })); const guard = (validator: () => string | null, next: Step) => () => { @@ -85,7 +84,7 @@ export function Onboarding({ const submit = async (event: FormEvent) => { event.preventDefault(); - const message = validateAll(draft); + const message = validateAll(draft, availableProviders); if (message) { setError(message); return; @@ -158,11 +157,11 @@ export function Onboarding({ )} {step === "provider" && ( validateProvider(draft), "company")} + onNext={guard(() => validateProvider(draft, availableProviders), "company")} /> )} {step === "company" && ( @@ -231,6 +230,11 @@ function ProviderStep({ onBack: () => void; onNext: () => void; }) { + const selected = providers.find((entry) => entry.key === draft.providerKey) ?? providers[0]; + if (!selected) throw new Error("No provider available for onboarding."); + const isOAuthLike = selected.authType === "oauth_pkce" || selected.authType === "account_subscription"; + const secretLabel = isOAuthLike ? "Access token" : "API key"; + return (

Provider

@@ -245,18 +249,33 @@ function ProviderStep({ className={`option-card ${draft.providerKey === provider.key ? "active" : ""}`.trim()} onClick={() => onChange({ providerKey: provider.key, - providerModel: provider.defaultModel ?? draft.providerModel, + providerModel: provider.defaultModel ?? provider.models[0]?.id ?? "", + providerSecret: "", })} >
{provider.label}
- {provider.defaultModel ?? "model"} + {authBadge(provider.authType)}
-
{provider.capabilityTags.join(" · ") || "API key"}
+
{provider.capabilityTags.join(" · ") || authBadge(provider.authType)}
))}
- + {selected.credentialHelp && ( +
{selected.credentialHelp}
+ )} + {isOAuthLike && selected.signInUrl && ( +
+ + {selected.signInUrl} +
+ )} + - {providers.find((entry) => entry.key === draft.providerKey)?.models.length ? ( + {selected.models.length ? ( @@ -285,6 +304,14 @@ function ProviderStep({ ); } +function authBadge(authType: string): string { + if (authType === "api_key") return "API key"; + if (authType === "oauth_pkce") return "OAuth"; + if (authType === "account_subscription") return "Account"; + if (authType === "cloud") return "Cloud"; + return authType; +} + function CompanyStep({ draft, onChange, onBack, onNext }: StepProps) { return (
@@ -385,9 +412,13 @@ function requireProvider(providers: ProviderCatalogEntry[], key: string): Provid return provider; } -function validateProvider(draft: Draft): string | null { +function validateProvider(draft: Draft, providers: ProviderCatalogEntry[]): string | null { if (!draft.providerKey) return "Pick a provider."; - if (!draft.providerSecret.trim()) return "API key is required."; + const selected = providers.find((entry) => entry.key === draft.providerKey); + if (!selected) return "Provider is no longer available."; + if (!draft.providerSecret.trim()) { + return selected.authType === "api_key" ? "API key is required." : "Access token is required."; + } if (!draft.providerModel.trim()) return "Model is required."; return null; } @@ -399,6 +430,6 @@ function validateCompany(draft: Draft): string | null { return null; } -function validateAll(draft: Draft): string | null { - return validateProvider(draft) ?? validateCompany(draft) ?? (!draft.goalTitle.trim() ? "Goal is required." : null); +function validateAll(draft: Draft, providers: ProviderCatalogEntry[]): string | null { + return validateProvider(draft, providers) ?? validateCompany(draft) ?? (!draft.goalTitle.trim() ? "Goal is required." : null); } diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index 77e98d63..36636832 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -49,6 +49,7 @@ export interface DesktopApi { markChatSpaceRead(input: MarkChatSpaceReadInput): Promise>; saveProviderCredential(input: SaveProviderCredentialInput): Promise>; launchProviderSignIn(input: LaunchProviderSignInInput): Promise>; + openExternalUrl(url: string): Promise>; createProviderResponse(input: CreateProviderResponseInput): Promise>; importSkillDirectory(input: ImportSkillDirectoryInput): Promise>; bindSkill(input: BindSkillInput): Promise>; diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index 3d050bfd..b73a5ccd 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -14,6 +14,7 @@ export const IPC_CHANNELS = { markChatSpaceRead: "agentcompany:mark-chat-space-read", saveProviderCredential: "agentcompany:save-provider-credential", launchProviderSignIn: "agentcompany:launch-provider-sign-in", + openExternalUrl: "agentcompany:open-external-url", createProviderResponse: "agentcompany:create-provider-response", importSkillDirectory: "agentcompany:import-skill-directory", bindSkill: "agentcompany:bind-skill",