/**
* First-run wizard (L5). Shown on the very first IDE launch: it offers the
* pre-filled, selectable reference profiles (Claude/Codex — only AIs drivable in
* structured mode, §17.6/D7) with **editable** commands, lets the user detect
* which CLIs are installed (✓/✗), then saves the chosen profiles and closes the
* first run.
*
* The candidate list is already filtered server-side (`reference_profiles` only
* exposes selectable profiles), so the wizard renders whatever it receives.
* Adding an arbitrary custom profile is no longer offered, since we cannot drive
* it in structured mode.
*
* Pure presentation: all behaviour comes from {@link useFirstRun} (the
* {@link ProfileGateway} port). Profile validation is the pure logic in
* `./profile`.
*/
import { useCallback, useEffect, useState } from "react";
import type {
AgentProfile,
GatewayError,
HttpChatConfig,
LocalModelServerConfig,
OpenCodeConfig,
OpenCodeProviderCatalogEntry,
} from "@/domain";
import { useGateways } from "@/app/di";
import { Button, IconButton, Input, Panel, Toolbar, cn } from "@/shared";
import {
ModelServersPanel,
ModelServerSelect,
useModelServers,
} from "@/features/model-servers";
import { useFirstRun, type WizardEntry } from "./useFirstRun";
import {
defaultHttpChatConfig,
defaultOpenCodeConfig,
parseArgs,
validateProfile,
type ProfileErrors,
} from "./profile";
/** A small caption above a control. */
function Caption({ children }: { children: React.ReactNode }) {
return {children};
}
function describeError(e: unknown): string {
if (e && typeof e === "object" && "message" in e) {
return String((e as GatewayError).message);
}
return String(e);
}
/** View-model for the OpenCode cloud-provider catalogue (ticket #92). */
interface OpenCodeProviderCatalog {
providers: OpenCodeProviderCatalogEntry[] | null;
loading: boolean;
error: string | null;
reload: () => void;
}
/**
* Loads the static OpenCode cloud-provider catalogue once for the whole
* wizard (every Cloud row shares it), so the provider/model pickers can be
* populated. Exposes a `reload` for the blocking "Réessayer" state.
*/
function useOpenCodeProviderCatalog(): OpenCodeProviderCatalog {
const { profile } = useGateways();
const [providers, setProviders] = useState(
null,
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
setProviders(await profile.listOpenCodeProviders());
} catch (e) {
setProviders(null);
setError(describeError(e));
} finally {
setLoading(false);
}
}, [profile]);
useEffect(() => {
void load();
}, [load]);
return { providers, loading, error, reload: () => void load() };
}
/**
* Renders the wizard when it is the first run. Calls `onDone` once the user
* finishes (so the host can drop the wizard and show the normal UI). Returns
* Returns `null` while loading. By default it also returns `null` once the first
* run is done (auto-show path in {@link App}); pass `forceOpen` to render it
* regardless — used by "Settings ▸ Configure profiles" to reopen the wizard after
* the first run.
*/
export function FirstRunWizard({
onDone,
forceOpen = false,
}: {
onDone?: () => void;
/** Render the wizard even when it is no longer the first run. */
forceOpen?: boolean;
}) {
// Mode is explicit (ticket #44), driven by the entry point — never inferred
// from `isFirstRun`: reopening from Settings (`forceOpen`) is the "edit" mode,
// which pre-loads and pre-selects the already-configured profiles.
const vm = useFirstRun(forceOpen ? "edit" : "firstRun");
const modelServers = useModelServers();
const providerCatalog = useOpenCodeProviderCatalog();
if (vm.isFirstRun === null) return null;
if (!forceOpen && vm.isFirstRun === false) return null;
async function finish() {
await vm.finish();
onDone?.();
}
return (
Welcome to IdeA
Choose which AI CLIs to configure. Commands are pre-filled and
editable.
}
>
{vm.error && (
{vm.error}
)}
{/* `detecting` deliberately does NOT disable this button (ticket #28):
detection is best-effort and may never answer. */}
{vm.detecting && (
Detecting…
)}
{/* F36: declare several local OpenCode profiles. Each click clones the
canonical `opencode-llamacpp` seed into a new, editable row. */}
{/* F35.2 — declare/edit/delete the local llama.cpp servers an OpenCode
profile can bind to. Sits above the profile list so a server exists
before it is picked in the OpenCode dropdown. */}
);
}
/** One editable candidate row: select, edit command/args, see availability. */
function ProfileRow({
entry,
servers,
providerCatalog,
onToggle,
onChange,
onRemove,
onDuplicate,
}: {
entry: WizardEntry;
/** Declared local model servers (F35.2), for the OpenCode binding dropdown. */
servers: LocalModelServerConfig[];
/** OpenCode cloud-provider catalogue (ticket #92), shared across rows. */
providerCatalog: OpenCodeProviderCatalog;
onToggle: () => void;
onChange: (p: AgentProfile) => void;
onRemove: () => void;
/** Present only for OpenCode rows: clone this row into a new profile (F36). */
onDuplicate?: () => void;
}) {
const { profile, selected, available } = entry;
const errors = validateProfile(profile);
const isOpenCode = profile.structuredAdapter === "openCode";
return (
{available === null ? "—" : available ? "✓ installed" : "✗ not found"}
{onDuplicate && (
)}
×
{/* F36: an OpenCode profile's name is identity-neutral but user-facing, so
it is editable per profile (several local models coexist). */}
{isOpenCode && (
)}
{profile.structuredAdapter === "openAiCompatible" && (
)}
{profile.structuredAdapter === "openCode" && (
)}
);
}
/**
* Segmented control (F — ticket #92) choosing whether an OpenCode profile runs
* against the local `llama.cpp` endpoint or a cloud provider from the OpenCode
* registry, and renders the matching sub-form. The two sub-forms never overlap;
* switching segments keeps the inactive one's draft in memory (component-local
* state) without touching `profile` until it is actually submitted.
*/
function OpenCodeModeFields({
profile,
errors,
servers,
providerCatalog,
onChange,
}: {
profile: AgentProfile;
errors: ProfileErrors;
servers: LocalModelServerConfig[];
providerCatalog: OpenCodeProviderCatalog;
onChange: (p: AgentProfile) => void;
}) {
const [mode, setMode] = useState<"local" | "cloud">(
profile.opencodeProvider ? "cloud" : "local",
);
return (