"use client"; import { ArrowUpCircleIcon, CopyIcon, DownloadIcon, LoaderIcon, LockIcon, LockOpenIcon, PlusIcon, Trash2Icon, XIcon,} from "lucide-react";import * as Arr from "effect/Array";import * as Result from "effect/Result";import { useEffect, useRef, useState, type ReactNode } from "react";import { isProviderDriverKind, resolveProviderInstanceEnabled, type ProviderInstanceConfig, type ProviderInstanceEnvironmentVariable, type ProviderInstanceId, type ProviderDriverKind, type ServerProvider, type ServerProviderModel,} from "@t3tools/contracts"; import { type CustomModelDefinition, readCustomModelEntries, toCustomModelSetting,} from "@t3tools/shared/model";import { cn } from "../../lib/utils";import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";import { normalizeProviderAccentColor } from "../../providerInstances";import { Badge } from "../ui/badge";import { Button } from "../ui/button";import { DraftInput } from "../ui/draft-input";import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";import { Switch } from "../ui/switch";import { stackedThreadToast, toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import type { DriverOption, ProviderEnvironmentFieldDefinition } from "./providerDriverMeta";import { ProviderSettingsForm } from "./ProviderSettingsForm";import { ProviderModelsSection } from "./ProviderModelsSection";import { ProviderInstanceIcon, providerInstanceInitials } from "../chat/ProviderInstanceIcon";import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker";import { RedactedSensitiveText } from "./RedactedSensitiveText";import { SettingsRow, SettingsSection } from "./settingsLayout";import { getProviderVersionAdvisoryPresentation, PROVIDER_STATUS_STYLES, getProviderSummary, getProviderVersionLabel, type ProviderStatusKey,} from "./providerStatus"; const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; let environmentVariableDraftId = 0;const nextEnvironmentVariableDraftId = () => `provider-env-${environmentVariableDraftId++}`; type EnvironmentDraftRow = { readonly id: string; readonly name: string; readonly value: string; readonly sensitive: boolean; readonly valueRedacted?: boolean;}; function makeEnvironmentDraftRow( variable: ProviderInstanceEnvironmentVariable, index: number,): EnvironmentDraftRow { return { id: `${index}:${variable.name}`, name: variable.name, value: variable.value, sensitive: variable.sensitive, ...(variable.valueRedacted !== undefined ? { valueRedacted: variable.valueRedacted } : {}), };} function providerEnvironmentsEqual( left: ReadonlyArray<ProviderInstanceEnvironmentVariable>, right: ReadonlyArray<ProviderInstanceEnvironmentVariable>,): boolean { return ( left.length === right.length && left.every((variable, index) => { const other = right[index]; return ( other !== undefined && variable.name === other.name && variable.value === other.value && variable.sensitive === other.sensitive && variable.valueRedacted === other.valueRedacted ); }) );} /** * Read `customModels` from the opaque config blob. The concrete driver * schemas type it as `CustomModelSetting[]`, but it arrives here as * `Schema.Unknown`, so the shared reader does the shape checking. */function readConfigCustomModels(config: unknown): ReadonlyArray<CustomModelDefinition> { if (config === null || typeof config !== "object") return []; return readCustomModelEntries((config as Record<string, unknown>).customModels);} /** * Set `key` to an arbitrary value on the opaque config blob. Unlike * provider settings field updates, does not drop empty-looking values — the * caller is responsible for deciding whether an empty array / empty * object should be stored explicitly (e.g. `customModels: []` is a * meaningful "user cleared their custom list" state distinct from * "driver default"). */function nextConfigBlobWithValue( config: unknown, key: string, value: unknown,): Record<string, unknown> { const base: Record<string, unknown> = config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {}; base[key] = value; return base;} /** * Custom rows come from current settings so name/descriptor edits show * instantly; a bare entry falls back to the live row's driver-default * capabilities (the server fills those in on its next probe). */export function deriveProviderModelsForDisplay(input: { readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined; readonly customModels: ReadonlyArray<CustomModelDefinition>;}): ReadonlyArray<ServerProviderModel> { const liveCustomModelsBySlug = new Map( Arr.filterMap(input.liveModels ?? [], (model) => model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid, ), ); const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? []; const customModels = input.customModels.map((entry) => ({ slug: entry.slug, name: entry.name, isCustom: true, capabilities: entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null, })); return [...serverModels, ...customModels];} function ProviderAuthEmail(props: { readonly email: string | undefined }) { const email = props.email?.trim(); if (!email) return null; return ( <RedactedSensitiveText value={email} ariaLabel="Toggle account email visibility" revealTooltip="Click to reveal email" hideTooltip="Click to hide email" className="max-w-full truncate" /> );} export function readProviderEnvironmentVariable( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, name: string,): ProviderInstanceEnvironmentVariable | undefined { return environment?.find((variable) => variable.name === name);} export function providerEnvironmentWithoutNames( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, names: ReadonlySet<string>,): ReadonlyArray<ProviderInstanceEnvironmentVariable> { return (environment ?? []).filter((variable) => !names.has(variable.name));} export function nextProviderEnvironmentWithFieldValue( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, field: ProviderEnvironmentFieldDefinition, value: string,): ReadonlyArray<ProviderInstanceEnvironmentVariable> { const trimmed = value.trim(); const next: ProviderInstanceEnvironmentVariable[] = []; let found = false; for (const variable of environment ?? []) { if (variable.name !== field.name) { next.push(variable); continue; } found = true; if (trimmed.length > 0) { next.push({ name: variable.name, value: trimmed, sensitive: field.sensitive ?? true, }); } } if (!found && trimmed.length > 0) { next.push({ name: field.name, value: trimmed, sensitive: field.sensitive ?? true, }); } return next;} function ProviderEnvironmentFieldRow(props: { readonly field: ProviderEnvironmentFieldDefinition; readonly variable: ProviderInstanceEnvironmentVariable | undefined; readonly idPrefix: string; readonly onCommit: (field: ProviderEnvironmentFieldDefinition, value: string) => void; readonly onRemove: (field: ProviderEnvironmentFieldDefinition) => void;}) { const inputId = `${props.idPrefix}-environment-${props.field.name}`; const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? ""); const placeholder = props.variable?.valueRedacted ? "Stored secret - enter a new value to replace" : props.field.placeholder; return ( <label htmlFor={inputId} className="block"> <span className="text-xs font-medium text-foreground">{props.field.label}</span> <div className="mt-1.5 flex min-w-0 items-center gap-2"> <DraftInput id={inputId} className="min-w-0 flex-1" type={props.field.sensitive === false ? undefined : "password"} autoComplete="off" value={value} onCommit={(next) => props.onCommit(props.field, next)} placeholder={placeholder} spellCheck={false} /> {props.variable ? ( <Button type="button" size="icon-sm" variant="ghost" className="size-8 shrink-0 text-muted-foreground hover:text-destructive" onClick={() => props.onRemove(props.field)} aria-label={`Clear ${props.field.label}`} > <XIcon className="size-3.5" /> </Button> ) : null} </div> {props.field.description ? ( <span className="mt-1 block text-xs text-muted-foreground">{props.field.description}</span> ) : null} </label> );} function ProviderEnvironmentSection(props: { readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>; readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void;}) { const [rows, setRows] = useState<ReadonlyArray<EnvironmentDraftRow>>(() => props.environment.map(makeEnvironmentDraftRow), ); const previousEnvironmentRef = useRef(props.environment); const lastPublishedEnvironmentRef = useRef< ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined >(undefined); useEffect(() => { const previousEnvironment = previousEnvironmentRef.current; const lastPublishedEnvironment = lastPublishedEnvironmentRef.current; previousEnvironmentRef.current = props.environment; lastPublishedEnvironmentRef.current = undefined; if ( previousEnvironment === props.environment || providerEnvironmentsEqual(previousEnvironment, props.environment) || (lastPublishedEnvironment !== undefined && providerEnvironmentsEqual(lastPublishedEnvironment, props.environment)) ) { return; } setRows(props.environment.map(makeEnvironmentDraftRow)); }, [props.environment]); const publishRows = (nextRows: ReadonlyArray<EnvironmentDraftRow>) => { const published: ProviderInstanceEnvironmentVariable[] = []; for (const row of nextRows) { const name = row.name.trim(); if (!ENVIRONMENT_VARIABLE_NAME_PATTERN.test(name)) { if ( name.length > 0 || row.value.length > 0 || row.sensitive !== true || row.valueRedacted !== undefined ) { return; } continue; } const { id: _id, ...rest } = row; published.push({ ...rest, name }); } lastPublishedEnvironmentRef.current = published; props.onChange(published); }; const updateVariable = (id: string, patch: Partial<Omit<EnvironmentDraftRow, "id">>) => { const nextRows = rows.map((row) => row.id === id ? { ...row, ...patch, ...(patch.value !== undefined ? { valueRedacted: false } : {}), } : row, ); setRows(nextRows); publishRows(nextRows); }; const removeVariable = (id: string) => { const nextRows = rows.filter((row) => row.id !== id); setRows(nextRows); publishRows(nextRows); }; const addVariable = () => setRows([ ...rows, { id: nextEnvironmentVariableDraftId(), name: "", value: "", sensitive: true, }, ]); return ( <div className="mt-3 min-w-0 space-y-2"> {rows.map((variable, index) => ( <div key={variable.id} className="flex min-w-0 items-center gap-1.5"> <DraftInput size="sm" className="w-44 shrink-0 font-mono" value={variable.name} onCommit={(name) => updateVariable(variable.id, { name: name.trim() })} placeholder="VARIABLE_NAME" spellCheck={false} aria-label={`Environment variable name ${index + 1}`} /> <span className="text-xs text-muted-foreground" aria-hidden> = </span> <DraftInput size="sm" className="min-w-0 flex-1 font-mono" value={variable.valueRedacted ? "" : variable.value} onCommit={(value) => updateVariable(variable.id, { value })} type={variable.sensitive ? "password" : undefined} autoComplete="off" placeholder={ variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value" } spellCheck={false} aria-label={`Environment variable value ${index + 1}`} /> <Tooltip> <TooltipTrigger render={ <Button type="button" size="icon-micro" variant="ghost-muted" className={cn( "[--control-icon-color:currentColor]", variable.sensitive && "text-foreground", )} onClick={() => { const sensitive = !variable.sensitive; updateVariable(variable.id, { sensitive, ...(sensitive && variable.valueRedacted === undefined ? {} : { valueRedacted: sensitive ? variable.valueRedacted : false }), }); }} aria-pressed={variable.sensitive} aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`} > {variable.sensitive ? ( <LockIcon className="size-3" /> ) : ( <LockOpenIcon className="size-3" /> )} </Button> } /> <TooltipPopup side="top"> {variable.sensitive ? "Sensitive, stored separately" : "Plain text"} </TooltipPopup> </Tooltip> <Button type="button" size="icon-micro" variant="ghost-muted" className="[--control-icon-color:currentColor] hover:text-destructive" onClick={() => removeVariable(variable.id)} aria-label={`Remove environment variable ${variable.name || index + 1}`} > <XIcon className="size-3" /> </Button> </div> ))} <div className="flex min-h-[1.875rem] flex-wrap items-center justify-end gap-x-3 gap-y-1"> {rows.length > 0 ? ( <span className="mr-auto text-xs text-muted-foreground"> Sensitive values are stored separately and never returned to the app. </span> ) : null} <Button type="button" size="xs" variant="ghost-muted" onClick={addVariable}> <PlusIcon className="size-3" /> Add variable </Button> </div> </div> );} interface ProviderInstanceCardProps { readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; readonly driverOption: DriverOption | undefined; readonly liveProvider: ServerProvider | undefined; readonly mode: "list" | "editor"; readonly selected?: boolean | undefined; readonly onSelect?: (() => void) | undefined; readonly readOnly?: boolean | undefined; readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void; /** * Pass `undefined` to hide the delete footer entirely. Built-in default * instance slots use `undefined` — they can't be deleted without losing * the slot, and their "reset to defaults" affordance lives on an outer * reset button instead. Explicit `| undefined` in the type accommodates * `exactOptionalPropertyTypes: true`, where an absent key and * `{ onDelete: undefined }` are treated as distinct shapes. */ readonly onDelete?: (() => void) | undefined; /** * Optional outer reset button rendered next to the driver icon. Built-in * default slots supply a reset-to-factory control here; custom instances * omit it. */ readonly headerAction?: ReactNode | undefined; readonly setup?: ReactNode; readonly hiddenModels: ReadonlyArray<string>; readonly favoriteModels: ReadonlyArray<string>; readonly modelOrder: ReadonlyArray<string>; readonly onHiddenModelsChange: (next: ReadonlyArray<string>) => void; readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void; readonly onModelOrderChange: (next: ReadonlyArray<string>) => void; readonly onRunUpdate?: (() => void) | undefined; readonly isUpdating?: boolean | undefined;} /** * Renders one provider instance as either a compact selectable list row or * the full editor shown beside that list. Both modes use the same enabled * state and provider metadata. * * Behavior notes: * - `liveProvider` is matched by the caller via `instanceId`; when no * match is available (e.g. the server hasn't probed yet, or the * driver is not shipped by the current build) the card still renders * with a neutral "checking" summary. * - Unknown drivers (`driverOption === undefined`) get a read-only * notice instead of editable fields, so fork instances round-trip * without accidentally destroying their config. * - The enabled Switch writes to the envelope's `instance.enabled` * field, which is the single enabled flag: the server folds any legacy * driver-specific `config.enabled` into the envelope on load and both * sides resolve through `resolveProviderInstanceEnabled` (an explicit * false wins, then envelope, then config, then the driver default). */export function ProviderInstanceCard({ instanceId, instance, driverOption, liveProvider, mode,Lines 1–500 of 1024. Load more
"use client"; import { ArrowUpCircleIcon, CopyIcon, DownloadIcon, LoaderIcon, LockIcon, LockOpenIcon, PlusIcon, Trash2Icon, XIcon, } from "lucide-react"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { isProviderDriverKind, resolveProviderInstanceEnabled, type ProviderInstanceConfig, type ProviderInstanceEnvironmentVariable, type ProviderInstanceId, type ProviderDriverKind, type ServerProvider, type ServerProviderModel, } from "@t3tools/contracts"; import { type CustomModelDefinition, readCustomModelEntries, toCustomModelSetting, } from "@t3tools/shared/model"; import { cn } from "../../lib/utils"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { DraftInput } from "../ui/draft-input"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { Switch } from "../ui/switch"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import type { DriverOption, ProviderEnvironmentFieldDefinition } from "./providerDriverMeta"; import { ProviderSettingsForm } from "./ProviderSettingsForm"; import { ProviderModelsSection } from "./ProviderModelsSection"; import { ProviderInstanceIcon, providerInstanceInitials } from "../chat/ProviderInstanceIcon"; import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker"; import { RedactedSensitiveText } from "./RedactedSensitiveText"; import { SettingsRow, SettingsSection } from "./settingsLayout"; import { getProviderVersionAdvisoryPresentation, PROVIDER_STATUS_STYLES, getProviderSummary, getProviderVersionLabel, type ProviderStatusKey, } from "./providerStatus"; const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; let environmentVariableDraftId = 0; const nextEnvironmentVariableDraftId = () => `provider-env-${environmentVariableDraftId++}`; type EnvironmentDraftRow = { readonly id: string; readonly name: string; readonly value: string; readonly sensitive: boolean; readonly valueRedacted?: boolean; }; function makeEnvironmentDraftRow( variable: ProviderInstanceEnvironmentVariable, index: number, ): EnvironmentDraftRow { return { id: `${index}:${variable.name}`, name: variable.name, value: variable.value, sensitive: variable.sensitive, ...(variable.valueRedacted !== undefined ? { valueRedacted: variable.valueRedacted } : {}), }; } function providerEnvironmentsEqual( left: ReadonlyArray<ProviderInstanceEnvironmentVariable>, right: ReadonlyArray<ProviderInstanceEnvironmentVariable>, ): boolean { return ( left.length === right.length && left.every((variable, index) => { const other = right[index]; return ( other !== undefined && variable.name === other.name && variable.value === other.value && variable.sensitive === other.sensitive && variable.valueRedacted === other.valueRedacted ); }) ); } /** * Read `customModels` from the opaque config blob. The concrete driver * schemas type it as `CustomModelSetting[]`, but it arrives here as * `Schema.Unknown`, so the shared reader does the shape checking. */ function readConfigCustomModels(config: unknown): ReadonlyArray<CustomModelDefinition> { if (config === null || typeof config !== "object") return []; return readCustomModelEntries((config as Record<string, unknown>).customModels); } /** * Set `key` to an arbitrary value on the opaque config blob. Unlike * provider settings field updates, does not drop empty-looking values — the * caller is responsible for deciding whether an empty array / empty * object should be stored explicitly (e.g. `customModels: []` is a * meaningful "user cleared their custom list" state distinct from * "driver default"). */ function nextConfigBlobWithValue( config: unknown, key: string, value: unknown, ): Record<string, unknown> { const base: Record<string, unknown> = config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {}; base[key] = value; return base; } /** * Custom rows come from current settings so name/descriptor edits show * instantly; a bare entry falls back to the live row's driver-default * capabilities (the server fills those in on its next probe). */ export function deriveProviderModelsForDisplay(input: { readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined; readonly customModels: ReadonlyArray<CustomModelDefinition>; }): ReadonlyArray<ServerProviderModel> { const liveCustomModelsBySlug = new Map( Arr.filterMap(input.liveModels ?? [], (model) => model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid, ), ); const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? []; const customModels = input.customModels.map((entry) => ({ slug: entry.slug, name: entry.name, isCustom: true, capabilities: entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null, })); return [...serverModels, ...customModels]; } function ProviderAuthEmail(props: { readonly email: string | undefined }) { const email = props.email?.trim(); if (!email) return null; return ( <RedactedSensitiveText value={email} ariaLabel="Toggle account email visibility" revealTooltip="Click to reveal email" hideTooltip="Click to hide email" className="max-w-full truncate" /> ); } export function readProviderEnvironmentVariable( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, name: string, ): ProviderInstanceEnvironmentVariable | undefined { return environment?.find((variable) => variable.name === name); } export function providerEnvironmentWithoutNames( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, names: ReadonlySet<string>, ): ReadonlyArray<ProviderInstanceEnvironmentVariable> { return (environment ?? []).filter((variable) => !names.has(variable.name)); } export function nextProviderEnvironmentWithFieldValue( environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, field: ProviderEnvironmentFieldDefinition, value: string, ): ReadonlyArray<ProviderInstanceEnvironmentVariable> { const trimmed = value.trim(); const next: ProviderInstanceEnvironmentVariable[] = []; let found = false; for (const variable of environment ?? []) { if (variable.name !== field.name) { next.push(variable); continue; } found = true; if (trimmed.length > 0) { next.push({ name: variable.name, value: trimmed, sensitive: field.sensitive ?? true, }); } } if (!found && trimmed.length > 0) { next.push({ name: field.name, value: trimmed, sensitive: field.sensitive ?? true, }); } return next; } function ProviderEnvironmentFieldRow(props: { readonly field: ProviderEnvironmentFieldDefinition; readonly variable: ProviderInstanceEnvironmentVariable | undefined; readonly idPrefix: string; readonly onCommit: (field: ProviderEnvironmentFieldDefinition, value: string) => void; readonly onRemove: (field: ProviderEnvironmentFieldDefinition) => void; }) { const inputId = `${props.idPrefix}-environment-${props.field.name}`; const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? ""); const placeholder = props.variable?.valueRedacted ? "Stored secret - enter a new value to replace" : props.field.placeholder; return ( <label htmlFor={inputId} className="block"> <span className="text-xs font-medium text-foreground">{props.field.label}</span> <div className="mt-1.5 flex min-w-0 items-center gap-2"> <DraftInput id={inputId} className="min-w-0 flex-1" type={props.field.sensitive === false ? undefined : "password"} autoComplete="off" value={value} onCommit={(next) => props.onCommit(props.field, next)} placeholder={placeholder} spellCheck={false} /> {props.variable ? ( <Button type="button" size="icon-sm" variant="ghost" className="size-8 shrink-0 text-muted-foreground hover:text-destructive" onClick={() => props.onRemove(props.field)} aria-label={`Clear ${props.field.label}`} > <XIcon className="size-3.5" /> </Button> ) : null} </div> {props.field.description ? ( <span className="mt-1 block text-xs text-muted-foreground">{props.field.description}</span> ) : null} </label> ); } function ProviderEnvironmentSection(props: { readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>; readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void; }) { const [rows, setRows] = useState<ReadonlyArray<EnvironmentDraftRow>>(() => props.environment.map(makeEnvironmentDraftRow), ); const previousEnvironmentRef = useRef(props.environment); const lastPublishedEnvironmentRef = useRef< ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined >(undefined); useEffect(() => { const previousEnvironment = previousEnvironmentRef.current; const lastPublishedEnvironment = lastPublishedEnvironmentRef.current; previousEnvironmentRef.current = props.environment; lastPublishedEnvironmentRef.current = undefined; if ( previousEnvironment === props.environment || providerEnvironmentsEqual(previousEnvironment, props.environment) || (lastPublishedEnvironment !== undefined && providerEnvironmentsEqual(lastPublishedEnvironment, props.environment)) ) { return; } setRows(props.environment.map(makeEnvironmentDraftRow)); }, [props.environment]); const publishRows = (nextRows: ReadonlyArray<EnvironmentDraftRow>) => { const published: ProviderInstanceEnvironmentVariable[] = []; for (const row of nextRows) { const name = row.name.trim(); if (!ENVIRONMENT_VARIABLE_NAME_PATTERN.test(name)) { if ( name.length > 0 || row.value.length > 0 || row.sensitive !== true || row.valueRedacted !== undefined ) { return; } continue; } const { id: _id, ...rest } = row; published.push({ ...rest, name }); } lastPublishedEnvironmentRef.current = published; props.onChange(published); }; const updateVariable = (id: string, patch: Partial<Omit<EnvironmentDraftRow, "id">>) => { const nextRows = rows.map((row) => row.id === id ? { ...row, ...patch, ...(patch.value !== undefined ? { valueRedacted: false } : {}), } : row, ); setRows(nextRows); publishRows(nextRows); }; const removeVariable = (id: string) => { const nextRows = rows.filter((row) => row.id !== id); setRows(nextRows); publishRows(nextRows); }; const addVariable = () => setRows([ ...rows, { id: nextEnvironmentVariableDraftId(), name: "", value: "", sensitive: true, }, ]); return ( <div className="mt-3 min-w-0 space-y-2"> {rows.map((variable, index) => ( <div key={variable.id} className="flex min-w-0 items-center gap-1.5"> <DraftInput size="sm" className="w-44 shrink-0 font-mono" value={variable.name} onCommit={(name) => updateVariable(variable.id, { name: name.trim() })} placeholder="VARIABLE_NAME" spellCheck={false} aria-label={`Environment variable name ${index + 1}`} /> <span className="text-xs text-muted-foreground" aria-hidden> = </span> <DraftInput size="sm" className="min-w-0 flex-1 font-mono" value={variable.valueRedacted ? "" : variable.value} onCommit={(value) => updateVariable(variable.id, { value })} type={variable.sensitive ? "password" : undefined} autoComplete="off" placeholder={ variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value" } spellCheck={false} aria-label={`Environment variable value ${index + 1}`} /> <Tooltip> <TooltipTrigger render={ <Button type="button" size="icon-micro" variant="ghost-muted" className={cn( "[--control-icon-color:currentColor]", variable.sensitive && "text-foreground", )} onClick={() => { const sensitive = !variable.sensitive; updateVariable(variable.id, { sensitive, ...(sensitive && variable.valueRedacted === undefined ? {} : { valueRedacted: sensitive ? variable.valueRedacted : false }), }); }} aria-pressed={variable.sensitive} aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`} > {variable.sensitive ? ( <LockIcon className="size-3" /> ) : ( <LockOpenIcon className="size-3" /> )} </Button> } /> <TooltipPopup side="top"> {variable.sensitive ? "Sensitive, stored separately" : "Plain text"} </TooltipPopup> </Tooltip> <Button type="button" size="icon-micro" variant="ghost-muted" className="[--control-icon-color:currentColor] hover:text-destructive" onClick={() => removeVariable(variable.id)} aria-label={`Remove environment variable ${variable.name || index + 1}`} > <XIcon className="size-3" /> </Button> </div> ))} <div className="flex min-h-[1.875rem] flex-wrap items-center justify-end gap-x-3 gap-y-1"> {rows.length > 0 ? ( <span className="mr-auto text-xs text-muted-foreground"> Sensitive values are stored separately and never returned to the app. </span> ) : null} <Button type="button" size="xs" variant="ghost-muted" onClick={addVariable}> <PlusIcon className="size-3" /> Add variable </Button> </div> </div> ); } interface ProviderInstanceCardProps { readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; readonly driverOption: DriverOption | undefined; readonly liveProvider: ServerProvider | undefined; readonly mode: "list" | "editor"; readonly selected?: boolean | undefined; readonly onSelect?: (() => void) | undefined; readonly readOnly?: boolean | undefined; readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void; /** * Pass `undefined` to hide the delete footer entirely. Built-in default * instance slots use `undefined` — they can't be deleted without losing * the slot, and their "reset to defaults" affordance lives on an outer * reset button instead. Explicit `| undefined` in the type accommodates * `exactOptionalPropertyTypes: true`, where an absent key and * `{ onDelete: undefined }` are treated as distinct shapes. */ readonly onDelete?: (() => void) | undefined; /** * Optional outer reset button rendered next to the driver icon. Built-in * default slots supply a reset-to-factory control here; custom instances * omit it. */ readonly headerAction?: ReactNode | undefined; readonly setup?: ReactNode; readonly hiddenModels: ReadonlyArray<string>; readonly favoriteModels: ReadonlyArray<string>; readonly modelOrder: ReadonlyArray<string>; readonly onHiddenModelsChange: (next: ReadonlyArray<string>) => void; readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void; readonly onModelOrderChange: (next: ReadonlyArray<string>) => void; readonly onRunUpdate?: (() => void) | undefined; readonly isUpdating?: boolean | undefined; } /** * Renders one provider instance as either a compact selectable list row or * the full editor shown beside that list. Both modes use the same enabled * state and provider metadata. * * Behavior notes: * - `liveProvider` is matched by the caller via `instanceId`; when no * match is available (e.g. the server hasn't probed yet, or the * driver is not shipped by the current build) the card still renders * with a neutral "checking" summary. * - Unknown drivers (`driverOption === undefined`) get a read-only * notice instead of editable fields, so fork instances round-trip * without accidentally destroying their config. * - The enabled Switch writes to the envelope's `instance.enabled` * field, which is the single enabled flag: the server folds any legacy * driver-specific `config.enabled` into the envelope on load and both * sides resolve through `resolveProviderInstanceEnabled` (an explicit * false wins, then envelope, then config, then the driver default). */ export function ProviderInstanceCard({ instanceId, instance, driverOption, liveProvider, mode,