/** * 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 type { AgentProfile, HttpChatConfig, LocalModelServerConfig, } from "@/domain"; import { useEffect, useState } from "react"; import { Button, IconButton, Input, Panel, Toolbar, cn } from "@/shared"; import { useI18n } from "@/i18n"; import { ModelServersPanel, useModelServers, } from "@/features/model-servers"; import { useFirstRun, type WizardEntry } from "./useFirstRun"; import { defaultHttpChatConfig, parseArgs, validateProfile, type ProfileErrors, } from "./profile"; import { OpenCodeModeFields, useOpenCodeProviderCatalog, type OpenCodeProviderCatalog, } from "./OpenCodeModeFields"; /** A small caption above a control. */ function Caption({ children }: { children: React.ReactNode }) { return {children}; } /** * 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; }) { const { tr } = useI18n(); // 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 (

{tr("Bienvenue dans IdeA", "Welcome to IdeA")}

{tr("Choisissez les CLI d’IA à configurer. Les commandes sont préremplies et modifiables.", "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 && ( {tr("Détection…", "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. */}
    {vm.entries.map((entry) => ( vm.toggle(entry.profile.id)} onChange={(p) => vm.updateProfile(entry.profile.id, p)} onRemove={() => vm.remove(entry.profile.id)} onDuplicate={ entry.profile.structuredAdapter === "openCode" ? () => vm.addOpenCodeProfile({ name: `${entry.profile.name} (${tr("copie", "copy")})`, opencode: entry.profile.opencode, }) : undefined } /> ))}
); } /** 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 { tr } = useI18n(); const { profile, selected, available } = entry; const errors = validateProfile(profile); const isOpenCode = profile.structuredAdapter === "openCode"; const canonicalArgsText = profile.args.join(" "); const [argsText, setArgsText] = useState(canonicalArgsText); useEffect(() => { setArgsText(canonicalArgsText); }, [canonicalArgsText, profile.id]); function commitArgs() { onChange({ ...profile, args: parseArgs(argsText) }); } return (
  • {available === null ? "—" : available ? tr("✓ installée", "✓ installed") : tr("✗ introuvable", "✗ 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" && ( )} {(profile.structuredAdapter === "codex" || profile.structuredAdapter === "claude") && (selected || profile.model) && ( )}
  • ); } function CliModelField({ profile, onChange, }: { profile: AgentProfile; onChange: (p: AgentProfile) => void; }) { const { tr } = useI18n(); return ( ); } /** Parses a number field: blank ⇒ `undefined` (backend applies its default). */ function parseOptInt(raw: string): number | undefined { const trimmed = raw.trim(); if (trimmed.length === 0) return undefined; const n = Number(trimmed); return Number.isFinite(n) ? n : NaN; } /** * The OpenAI-compatible (local/LAN) HTTP config section: endpoint, model, the * *name* of the API-key env var (never the key), and optional timeouts / tool * guard. Rendered only for a `openAiCompatible` profile; edits flow back through * `onChange` as a patched `chatHttp`. */ function HttpChatFields({ profile, errors, onChange, }: { profile: AgentProfile; errors: ProfileErrors; onChange: (p: AgentProfile) => void; }) { const { tr } = useI18n(); const http = profile.chatHttp ?? defaultHttpChatConfig(); const patch = (next: Partial) => onChange({ ...profile, chatHttp: { ...http, ...next } }); return (
    {tr("Modèle local / réseau (compatible OpenAI)", "Local / LAN model (OpenAI-compatible)")}
    ); }