806 lines · 30.6 KB · TypeScript
RawDownload
"use client"; import {  ArrowUpCircleIcon,  ChevronDownIcon,  CopyIcon,  DownloadIcon,  LoaderIcon,  PlusIcon,  Trash2Icon,  XIcon,} from "lucide-react";import * as Arr from "effect/Array";import * as Result from "effect/Result";import { useState, type ReactNode } from "react";import {  isProviderDriverKind,  type ProviderInstanceConfig,  type ProviderInstanceEnvironmentVariable,  type ProviderInstanceId,  type ProviderDriverKind,  type ServerProvider,  type ServerProviderModel,} from "@t3tools/contracts"; 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 { Checkbox } from "../ui/checkbox";import { Collapsible, CollapsibleContent } from "../ui/collapsible";import { DraftInput } from "../ui/draft-input";import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";import { ScrollArea } from "../ui/scroll-area";import { Switch } from "../ui/switch";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";import { stackedThreadToast, toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import type { DriverOption } from "./providerDriverMeta";import { ProviderSettingsForm } from "./ProviderSettingsForm";import { ProviderModelsSection } from "./ProviderModelsSection";import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker";import { RedactedSensitiveText } from "./RedactedSensitiveText";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 } : {}),  };} /** * Read a string[] at `key` from the opaque config blob, filtering out * non-string entries. Used for `customModels`, which is always typed as * `string[]` by the concrete driver schemas but arrives here as * `Schema.Unknown`. */function readConfigStringArray(config: unknown, key: string): ReadonlyArray<string> {  if (config === null || typeof config !== "object") return [];  const value = (config as Record<string, unknown>)[key];  if (!Array.isArray(value)) return [];  return value.filter((entry): entry is string => typeof entry === "string");} /** * 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;} export function deriveProviderModelsForDisplay(input: {  readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined;  readonly customModels: ReadonlyArray<string>;}): 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(    (slug) =>      liveCustomModelsBySlug.get(slug) ?? {        slug,        name: slug,        isCustom: true,        capabilities: null,      },  );  return [...serverModels, ...customModels];} function ProviderAuthEmail(props: {  readonly email: string | undefined;  readonly prefix?: string;  readonly separator?: boolean;}) {  const trimmed = props.email?.trim();  if (!trimmed) return null;   return (    <span className="inline-flex min-w-0 items-center gap-1.5">      {props.separator ? <span aria-hidden>·</span> : null}      {props.prefix ? <span className="text-muted-foreground/80">{props.prefix}</span> : null}      <RedactedSensitiveText        value={trimmed}        ariaLabel="Toggle account email visibility"        revealTooltip="Click to reveal email"        hideTooltip="Click to hide email"      />    </span>  );} function ProviderEnvironmentSection(props: {  readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>;  readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void;}) {  const [rows, setRows] = useState<ReadonlyArray<EnvironmentDraftRow>>(() =>    props.environment.map(makeEnvironmentDraftRow),  );   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 });    }    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);  };   return (    <div className="grid gap-2">      <div className="flex items-center justify-between gap-3">        <span className="text-xs font-medium text-foreground">Environment variables</span>        <Button          type="button"          size="sm"          variant="outline"          className="h-7 gap-1.5 px-2 text-xs"          onClick={() =>            setRows([              ...rows,              {                id: nextEnvironmentVariableDraftId(),                name: "",                value: "",                sensitive: true,              },            ])          }        >          <PlusIcon className="size-3" />          Add        </Button>      </div>      {rows.length === 0 ? (        <p className="text-xs text-muted-foreground">          Add variables to pass API keys, base URLs, or other per-instance CLI settings.        </p>      ) : (        <div className="overflow-hidden rounded-md border border-border/70">          <Table>            <TableHeader className="bg-muted/25 text-[11px] text-muted-foreground">              <TableRow className="hover:bg-transparent">                <TableHead>Variable</TableHead>                <TableHead>Value</TableHead>                <TableHead className="w-20">Sensitive</TableHead>                <TableHead className="w-12 text-right">                  <span className="sr-only">Options</span>                </TableHead>              </TableRow>            </TableHeader>            <TableBody>              {rows.map((variable, index) => (                <TableRow                  key={variable.id}                  className="border-border/60 odd:bg-muted/20 even:bg-background/20"                >                  <TableCell>                    <DraftInput                      value={variable.name}                      onCommit={(name) => updateVariable(variable.id, { name: name.trim() })}                      placeholder="VARIABLE_NAME"                      spellCheck={false}                      aria-label={`Environment variable name ${index + 1}`}                    />                  </TableCell>                  <TableCell>                    <DraftInput                      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}`}                    />                  </TableCell>                  <TableCell className="w-20">                    <div className="flex h-8 items-center justify-center">                      <Checkbox                        checked={variable.sensitive}                        onCheckedChange={(checked) => {                          const sensitive = Boolean(checked);                          updateVariable(variable.id, {                            sensitive,                            ...(sensitive && variable.valueRedacted === undefined                              ? {}                              : { valueRedacted: sensitive ? variable.valueRedacted : false }),                          });                        }}                        aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`}                      />                    </div>                  </TableCell>                  <TableCell className="w-12">                    <div className="flex justify-end">                      <Button                        type="button"                        size="icon-sm"                        variant="ghost"                        className="size-8 text-muted-foreground hover:text-destructive"                        onClick={() => removeVariable(variable.id)}                        aria-label={`Remove environment variable ${variable.name || index + 1}`}                      >                        <XIcon className="size-3.5" />                      </Button>                    </div>                  </TableCell>                </TableRow>              ))}            </TableBody>          </Table>        </div>      )}      <span className="text-xs text-muted-foreground">        Sensitive values are stored separately and are not returned to the app after saving.      </span>    </div>  );} interface ProviderInstanceCardProps {  readonly instanceId: ProviderInstanceId;  readonly instance: ProviderInstanceConfig;  readonly driverOption: DriverOption | undefined;  readonly liveProvider: ServerProvider | undefined;  readonly isExpanded: boolean;  readonly onExpandedChange: (open: boolean) => void;  readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void;  /**   * Pass `undefined` to hide the delete button 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 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;} /** * A single configured provider-instance row in the Providers settings * section. Used for every row — both the built-in default instance for a * driver (rendered with `onDelete` omitted) and user-authored custom * instances (`onDelete` supplied). The only UI difference between the two * is whether the trash button is visible; every other field (display * name, config fields, models) behaves identically. * * 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; the server's registry consults this at `entry.enabled ?? true` *     before materializing the instance, and the probe also checks its *     driver-specific `config.enabled`. We treat the envelope flag as the *     single source of truth from the UI — built-in cards used to write *     the inner flag, but on the promotion-to-instance path every edit *     flows through the envelope. */export function ProviderInstanceCard({  instanceId,  instance,  driverOption,  liveProvider,  isExpanded,  onExpandedChange,  onUpdate,  onDelete,  headerAction,  hiddenModels,  favoriteModels,  modelOrder,  onHiddenModelsChange,  onFavoriteModelsChange,  onModelOrderChange,  onRunUpdate,  isUpdating = false,}: ProviderInstanceCardProps) {  const enabled = instance.enabled ?? true;  // The server-reported status wins when present; otherwise fall back to  // "disabled"/"warning" based on the local `enabled` flag so the dot  // reflects the persisted intent even before the first probe completes.  const statusKey: ProviderStatusKey =    (liveProvider?.status as ProviderStatusKey | undefined) ?? (enabled ? "warning" : "disabled");  const statusStyle = PROVIDER_STATUS_STYLES[statusKey];  const rawSummary = getProviderSummary(liveProvider);  const authEmail = liveProvider?.auth.email;  const hasAuthenticatedEmail =    liveProvider?.auth.status === "authenticated" && Boolean(authEmail?.trim());  const authenticatedDetail = hasAuthenticatedEmail    ? (liveProvider?.auth.label ?? liveProvider?.auth.type ?? null)    : null;  const summary = rawSummary;  const versionLabel = getProviderVersionLabel(liveProvider?.version);  const versionAdvisory = getProviderVersionAdvisoryPresentation(liveProvider?.versionAdvisory);  const updateCommand = versionAdvisory?.updateCommand ?? null;  const FallbackIconComponent = driverOption?.icon;  const displayName =    instance.displayName?.trim() || driverOption?.label || String(instance.driver);  const accentColor = normalizeProviderAccentColor(instance.accentColor);  const { copyToClipboard } = useCopyToClipboard<{ providerName: string }>({    onCopy: ({ providerName }) => {      toastManager.add({        type: "success",        title: `${providerName} update command copied`,        description: "Run it in a terminal when you are ready to update.",      });    },    onError: (error, { providerName }) => {      toastManager.add(        stackedThreadToast({          type: "error",          title: `Could not copy ${providerName} update command`,          description: error.message,        }),      );    },  });   // Narrow `instance.driver` for callers that key on the closed  // `ProviderDriverKind` union (e.g. `normalizeModelSlug`'s alias table). Custom  // fork drivers pass through as `null` and those callers fall back to  // verbatim behaviour.  const driverKind: ProviderDriverKind | null = isProviderDriverKind(instance.driver)    ? instance.driver    : null;   const customModels = readConfigStringArray(instance.config, "customModels");  // Server-returned models may lag behind settings writes. Treat probe  // models as the source for built-ins only; custom rows come directly  // from the current instance config so add/remove reflects immediately.  const modelsForDisplay = deriveProviderModelsForDisplay({    liveModels: liveProvider?.models,    customModels,  });   const updateDisplayName = (value: string) => {    const trimmed = value.trim();    const { displayName: _omit, ...rest } = instance;    onUpdate(      trimmed.length > 0        ? ({ ...rest, displayName: trimmed } as ProviderInstanceConfig)        : (rest as ProviderInstanceConfig),    );  };   const updateEnabled = (value: boolean) => {    onUpdate({ ...instance, enabled: value });  };   const updateAccentColor = (value: string) => {    const normalized = normalizeProviderAccentColor(value);    const { accentColor: _omit, ...rest } = instance;    onUpdate(      normalized        ? ({ ...rest, accentColor: normalized } as ProviderInstanceConfig)        : (rest as ProviderInstanceConfig),    );  };   const updateConfig = (nextConfig: Record<string, unknown> | undefined) => {    const { config: _omit, ...rest } = instance;    onUpdate(      nextConfig !== undefined        ? ({ ...rest, config: nextConfig } as ProviderInstanceConfig)        : (rest as ProviderInstanceConfig),    );  };   const updateCustomModels = (next: ReadonlyArray<string>) => {    const nextConfig = nextConfigBlobWithValue(instance.config, "customModels", [...next]);    const { config: _omit, ...rest } = instance;    onUpdate({ ...rest, config: nextConfig } as ProviderInstanceConfig);  };   const updateEnvironment = (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => {    const cleaned = environment.filter((variable) => variable.name.trim().length > 0);    const { environment: _omit, ...rest } = instance;    onUpdate(      cleaned.length > 0        ? ({ ...rest, environment: cleaned } as ProviderInstanceConfig)        : (rest as ProviderInstanceConfig),

Lines 1–500 of 806.