"use client"; import { CheckIcon } from "lucide-react"; import { Radio as RadioPrimitive } from "@base-ui/react/radio"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ProviderInstanceId, ProviderDriverKind, type ProviderInstanceConfig, } from "@t3tools/contracts"; import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon } from "../Icons"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogTitle, } from "../ui/dialog"; import { Badge } from "../ui/badge"; import { Input } from "../ui/input"; import { RadioGroup } from "../ui/radio-group"; import { toastManager } from "../ui/toast"; import { DRIVER_OPTION_BY_VALUE, DRIVER_OPTIONS, type DriverOption } from "./providerDriverMeta"; const PROVIDER_ACCENT_SWATCHES = [ "#2563eb", "#16a34a", "#ea580c", "#dc2626", "#7c3aed", "#0891b2", ] as const; /** * Normalize a user-provided label into a slug suffix for the instance id. * The full id is formed by prefixing the driver slug — e.g. label "Work" on * driver "codex" becomes `codex_work`. Output is trimmed to 48 chars so the * final composed id stays under the 64-char slug cap enforced by * `ProviderInstanceId` in `@t3tools/contracts`. */ function slugifyLabel(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 48); } function deriveInstanceId(driver: ProviderDriverKind, label: string): string { const slug = slugifyLabel(label); return slug ? `${driver}_${slug}` : ""; } const INSTANCE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); const DEFAULT_DRIVER_OPTION = DRIVER_OPTIONS[0]!; const COMING_SOON_DRIVER_OPTIONS: readonly DriverOption[] = [ { value: ProviderDriverKind.make("githubCopilot"), label: "Github Copilot", icon: GithubCopilotIcon, fields: [], }, { value: ProviderDriverKind.make("gemini"), label: "Gemini", icon: Gemini, fields: [], }, { value: ProviderDriverKind.make("acpRegistry"), label: "ACP Registry", icon: ACPRegistryIcon, fields: [], }, { value: ProviderDriverKind.make("piAgent"), label: "Pi Agent", icon: PiAgentIcon, fields: [], }, ]; /** * Validate an instance id against the same slug rules the server applies in * `ProviderInstanceId` (see `packages/contracts/src/providerInstance.ts`). * Returns a user-facing error string, or `null` if valid. */ function validateInstanceId(id: string, existing: ReadonlySet): string | null { if (id.length === 0) return "Instance ID is required."; if (id.length > 64) return "Instance ID must be 64 characters or fewer."; if (!INSTANCE_ID_PATTERN.test(id)) { return "Instance ID must start with a letter and use only letters, digits, '-', or '_'."; } if (existing.has(id)) return `An instance named '${id}' already exists.`; return null; } interface AddProviderInstanceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { const settings = useSettings(); const { updateSettings } = useUpdateSettings(); const [wizardStep, setWizardStep] = useState(0); const [driver, setDriver] = useState(DEFAULT_DRIVER_KIND); const [label, setLabel] = useState(""); const [accentColor, setAccentColor] = useState(""); const [instanceId, setInstanceId] = useState(""); const [instanceIdDirty, setInstanceIdDirty] = useState(false); // Driver-specific field values keyed by `${driver}:${fieldKey}` so toggling // between drivers during the same dialog session doesn't lose in-progress // input. Only the active driver's values are persisted on save. const [fieldValues, setFieldValues] = useState>({}); // Errors are suppressed until the user has tried to submit once. After that // they update live so fixing the problem clears the message in place. const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false); const existingIds = useMemo( () => new Set(Object.keys(settings.providerInstances ?? {})), [settings.providerInstances], ); // Reset the form every time the dialog opens so each creation starts // from a clean slate. useEffect(() => { if (!open) return; setDriver(DEFAULT_DRIVER_KIND); setLabel(""); setAccentColor(""); setInstanceId(""); setWizardStep(0); setInstanceIdDirty(false); setFieldValues({}); setHasAttemptedSubmit(false); }, [open]); // Auto-derive the instance id from driver + label until the user types // in the Instance ID field directly (after which they own its value). useEffect(() => { if (instanceIdDirty) return; setInstanceId(deriveInstanceId(driver, label)); }, [driver, label, instanceIdDirty]); const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION; const instanceIdError = validateInstanceId(instanceId, existingIds); const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null; const previewLabel = label.trim() || `${driverOption.label} Workspace`; const wizardSteps = ["Driver", "Identity", "Config"] as const; const wizardStepSummaries = [driverOption.label, previewLabel, null] as const; const getFieldValue = useCallback( (fieldKey: string) => fieldValues[`${driver}:${fieldKey}`] ?? "", [driver, fieldValues], ); const setFieldValue = useCallback( (fieldKey: string, value: string) => { setFieldValues((existing) => ({ ...existing, [`${driver}:${fieldKey}`]: value, })); }, [driver], ); const handleSave = useCallback(() => { setHasAttemptedSubmit(true); if (instanceIdError !== null) return; // Build the config blob from non-empty driver-specific field values. // Empty strings are dropped so defaults remain in effect on the server. const config: Record = {}; for (const field of driverOption.fields) { const value = (fieldValues[`${driver}:${field.key}`] ?? "").trim(); if (value.length > 0) config[field.key] = value; } const hasConfig = Object.keys(config).length > 0; const normalizedAccentColor = normalizeProviderAccentColor(accentColor); const nextInstance: ProviderInstanceConfig = { driver, enabled: true, ...(label.trim().length > 0 ? { displayName: label.trim() } : {}), ...(normalizedAccentColor ? { accentColor: normalizedAccentColor } : {}), ...(hasConfig ? { config } : {}), }; // `ProviderInstanceId.make` revalidates the slug; we've already checked // it via `validateInstanceId`, but going through the brand constructor // keeps the type boundary honest and guards against any future drift in // the slug rules. const brandedId = ProviderInstanceId.make(instanceId); const nextMap = { ...settings.providerInstances, [brandedId]: nextInstance, }; try { updateSettings({ providerInstances: nextMap }); toastManager.add({ type: "success", title: "Provider instance added", description: `${driverOption.label} instance '${instanceId}' was added.`, }); onOpenChange(false); } catch (error) { toastManager.add({ type: "error", title: "Could not add provider instance", description: error instanceof Error ? error.message : "Update failed.", }); } }, [ driver, driverOption, fieldValues, instanceId, instanceIdError, label, accentColor, onOpenChange, settings.providerInstances, updateSettings, ]); return (
Add provider instance Configure an additional provider instance — for example, a second Codex install pointed at a different workspace.
{wizardSteps.map((step, index) => ( ))}
Driver setDriver(ProviderDriverKind.make(value))} aria-labelledby="add-instance-driver-label" className="grid grid-cols-2 gap-2.5" > {DRIVER_OPTIONS.map((option) => { const IconComponent = option.icon; const isSelected = option.value === driver; return ( {option.label} {option.badgeLabel ? ( {option.badgeLabel} ) : null} ); })} {COMING_SOON_DRIVER_OPTIONS.map((option) => { const IconComponent = option.icon; return ( {option.label} Coming Soon ); })}
Accent color
setAccentColor(event.target.value)} aria-label="Provider instance accent color" className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5" />
{PROVIDER_ACCENT_SWATCHES.map((swatch) => { const selected = accentColor.toLowerCase() === swatch; return (
{accentColor ? ( ) : null}
Optional marker shown in the picker.
{driverOption.fields.length > 0 ? (
{driverOption.fields.map((field) => ( ))}
) : wizardStep === 2 ? (

This driver has no required configuration. You can add the instance now.

) : null}
{wizardStep < wizardSteps.length - 1 ? ( ) : ( )}
); }