Skip to content
Source
Sign inSign up
Open History to see the last commit

ProviderInstanceCard.tsx

1,478 lines52.4 KB

1import { SettingsGroup } from "./SettingsGroup";
2import { Text } from "../ui/text";
3import { Spinner } from "~/components/ui/spinner";
4​
5import {
6 AlertTriangleIcon,
7 CheckIcon,
8 ArrowUpCircleIcon,
9 CopyIcon,
10 DownloadIcon,
11 ExternalLinkIcon,
12 LayersIcon,
13 PlusIcon,
14 Trash2Icon,
15 XIcon,
16 LockIcon,
17 LockOpenIcon,
18} from "lucide-react";
19import * as Arr from "effect/Array";
20import * as Result from "effect/Result";
21import { Fragment, useId, useState, type ReactElement, type ReactNode } from "react";
22import {
23 isProviderDriverKind,
24 resolveProviderInstanceEnabled,
25 type ProviderInstanceConfig,
26 type ProviderInstanceEnvironmentVariable,
27 type ProviderInstanceId,
28 type AcpRegistryUrlAuthAction,
29 type EnvironmentId,
30 type ProjectId,
31 type ProviderDriverKind,
32 type SecretReference,
33 type ServerProvider,
34 type ServerProviderModel,
35} from "@t3tools/contracts";
36​
37import {
38 type CustomModelDefinition,
39 readCustomModelEntries,
40 toCustomModelSetting,
41} from "@t3tools/shared/model";
42import { cn } from "../../lib/utils";
43import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
44import { useStableCallback } from "../../hooks/useStableCallback";
45import { normalizeProviderAccentColor } from "../../providerInstances";
46import { Badge } from "../ui/badge";
47import { Button } from "../ui/button";
48import { DraftInput } from "../ui/draft-input";
49import { SecretSourceInput, type SecretSource } from "./SecretSourceInput";
50import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
51import { StatusDot } from "../ui/status-dot";
52import { Switch } from "../ui/switch";
53import { ManagedControl, ManagedNotice } from "../ManagedPolicy";
54import { managedInstanceLocks, useManagedPolicy } from "../../state/managedPolicy";
55import { disallowedProviderMessage, isManagedDriverAllowed } from "@t3tools/contracts";
56import { stackedThreadToast, toastManager } from "../ui/toast";
57import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
58import type {
59 ProviderClientDefinition,
60 ProviderEnvironmentField,
61} from "@t3tools/provider-core/client";
62import {
63 deriveProviderSettingsFields,
64 ProviderSettingsForm,
65 reconcileRowsDraft,
66 seedRowsDraft,
67} from "./ProviderSettingsForm";
68import { ProviderModelsSection } from "./ProviderModelsSection";
69import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
70import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker";
71import { RedactedSensitiveText } from "./RedactedSensitiveText";
72import { SettingResetButton, SettingsRow, SettingsSection } from "./settingsLayout";
73import { AcpSessionManagementSection } from "./AcpSessionManagementSection";
74import { FoldedSettingsSection } from "./FoldedSettingsSection";
75import { readCodexSetupMode } from "./CodexSetupSection.logic";
76import { getProviderRestartNotice } from "../ProviderUpdateLaunchNotification.logic";
77import {
78 getProviderVersionAdvisoryPresentation,
79 PROVIDER_STATUS_STYLES,
80 getProviderStatusKey,
81 getProviderSummary,
82 getProviderVersionLabel,
83} from "./providerStatus";
84​
85const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
86​
87function ProviderStatusDiagnostic({
88 detail,
89 children,
90}: {
91 detail: ReactNode;
92 children: ReactElement;
93}) {
94 if (!detail) return children;
95 return (
96 <Tooltip>
97 <TooltipTrigger render={children} />
98 <TooltipPopup side="top">{detail}</TooltipPopup>
99 </Tooltip>
100 );
102​
103let environmentVariableDraftId = 0;
104const DRAFT_ROW_ID_PREFIX = "provider-env-";
105const nextEnvironmentVariableDraftId = () =>
106 `${DRAFT_ROW_ID_PREFIX}${environmentVariableDraftId++}`;
107​
108type EnvironmentDraftRow = {
109 readonly id: string;
110 readonly name: string;
111 readonly value: ProviderInstanceEnvironmentVariable["value"];
112 readonly sensitive: boolean;
113 readonly valueRedacted?: boolean;
114};
115​
116function makeEnvironmentDraftRow(
117 variable: ProviderInstanceEnvironmentVariable,
118 index: number,
119): EnvironmentDraftRow {
120 return {
121 id: `${index}:${variable.name}`,
122 name: variable.name,
123 value: variable.value,
124 sensitive: variable.sensitive,
125 ...(variable.valueRedacted !== undefined ? { valueRedacted: variable.valueRedacted } : {}),
126 };
128​
129const toEnvironmentDraftRows = (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) =>
130 environment.map(makeEnvironmentDraftRow);
131​
132function environmentValuesEqual(
133 left: ProviderInstanceEnvironmentVariable["value"],
134 right: ProviderInstanceEnvironmentVariable["value"],
135): boolean {
136 if (typeof left === "string" || typeof right === "string") return left === right;
137 return left.provider === right.provider && left.reference === right.reference;
139​
140function providerEnvironmentsEqual(
141 left: ReadonlyArray<ProviderInstanceEnvironmentVariable>,
142 right: ReadonlyArray<ProviderInstanceEnvironmentVariable>,
143): boolean {
144 return (
145 left.length === right.length &&
146 left.every((variable, index) => {
147 const other = right[index];
148 return (
149 other !== undefined &&
150 variable.name === other.name &&
151 environmentValuesEqual(variable.value, other.value) &&
152 variable.sensitive === other.sensitive &&
153 variable.valueRedacted === other.valueRedacted
154 );
155 })
156 );
158​
159function readConfigString(config: unknown, key: string): string | null {
160 if (config === null || typeof config !== "object") return null;
161 const value = (config as Record<string, unknown>)[key];
162 return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
164​
165/** Preserve explicit empty arrays/objects; clearing a custom list differs from selecting driver defaults. */
166function nextConfigBlobWithValue(
167 config: unknown,
168 key: string,
169 value: unknown,
170): Record<string, unknown> {
171 const base: Record<string, unknown> =
172 config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {};
173 base[key] = value;
174 return base;
176​
177export function deriveProviderModelsForDisplay(input: {
178 readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined;
179 readonly customModels: ReadonlyArray<CustomModelDefinition>;
180}): ReadonlyArray<ServerProviderModel> {
181 const liveCustomModelsBySlug = new Map(
182 Arr.filterMap(input.liveModels ?? [], (model) =>
183 model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid,
184 ),
185 );
186 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];
187 const serverSlugs = new Set(serverModels.map((model) => model.slug));
188 const customModels = input.customModels.flatMap((entry) =>
189 serverSlugs.has(entry.slug)
190 ? []
191 : [
192 {
193 slug: entry.slug,
194 name: entry.name,
195 isCustom: true,
196 capabilities:
197 entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null,
198 },
199 ],
200 );
201 return [...serverModels, ...customModels];
203​
204function ProviderAuthEmail(props: { readonly email: string | undefined }) {
205 const email = props.email?.trim();
206 if (!email) return null;
207​
208 return (
209 <RedactedSensitiveText
210 value={email}
211 ariaLabel="Toggle account email visibility"
212 revealTooltip="Click to reveal email"
213 hideTooltip="Click to hide email"
214 className="pointer-events-auto max-w-full truncate"
215 />
216 );
218​
219function readProviderEnvironmentVariable(
220 environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,
221 name: string,
222): ProviderInstanceEnvironmentVariable | undefined {
223 return environment?.find((variable) => variable.name === name);
225​
226function providerEnvironmentWithoutNames(
227 environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,
228 names: ReadonlySet<string>,
229): ReadonlyArray<ProviderInstanceEnvironmentVariable> {
230 return (environment ?? []).filter((variable) => !names.has(variable.name));
232​
233export function nextProviderEnvironmentWithFieldValue(
234 environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,
235 field: ProviderEnvironmentField,
236 value: string,
237): ReadonlyArray<ProviderInstanceEnvironmentVariable> {
238 const trimmed = value.trim();
239 const next: ProviderInstanceEnvironmentVariable[] = [];
240 let found = false;
241​
242 for (const variable of environment ?? []) {
243 if (variable.name !== field.name) {
244 next.push(variable);
245 continue;
246 }
247 found = true;
248 if (trimmed.length > 0) {
249 next.push({
250 name: variable.name,
251 value: trimmed,
252 sensitive: field.sensitive ?? true,
253 });
254 }
255 }
256​
257 if (!found && trimmed.length > 0) {
258 next.push({
259 name: field.name,
260 value: trimmed,
261 sensitive: field.sensitive ?? true,
262 });
263 }
264​
265 return next;
267​
268export function nextProviderEnvironmentWithFieldSecret(
269 environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,
270 field: ProviderEnvironmentField,
271 secret: SecretReference | undefined,
272): ReadonlyArray<ProviderInstanceEnvironmentVariable> {
273 const others = providerEnvironmentWithoutNames(environment, new Set([field.name]));
274 return secret === undefined
275 ? others
276 : [...others, { name: field.name, value: secret, sensitive: true }];
278​
279function ProviderEnvironmentFieldRow(props: {
280 readonly field: ProviderEnvironmentField;
281 readonly variable: ProviderInstanceEnvironmentVariable | undefined;
282 readonly idPrefix: string;
283 readonly onCommit: (field: ProviderEnvironmentField, value: string) => void;
284 readonly onSecretChange: (
285 field: ProviderEnvironmentField,
286 secret: SecretReference | undefined,
287 ) => void;
288 readonly onRemove: (field: ProviderEnvironmentField) => void;
289 readonly managedReason?: string | undefined;
290 readonly secretSources: ReadonlyArray<SecretSource>;
291}) {
292 const id = useId();
293 const inputId = `${props.idPrefix}-${id}-environment-${props.field.name}`;
294 const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? "");
295​
296 return (
297 <SettingsRow
298 title={<label htmlFor={inputId}>{props.field.label}</label>}
299 description={props.field.description}
300 managed={props.managedReason}
301 control={
302 <div className="flex w-full min-w-0 items-center gap-2 @min-[26rem]/settings-row:w-56">
303 {props.field.sensitive === false ? (
304 <DraftInput
305 id={inputId}
306 size="settings"
307 className="min-w-0 flex-1"
308 autoComplete="off"
309 value={typeof value === "string" ? value : ""}
310 onCommit={(next) => props.onCommit(props.field, next)}
311 placeholder={props.field.placeholder}
312 spellCheck={false}
313 />
314 ) : (
315 <SecretSourceInput
316 id={inputId}
317 size="settings"
318 className="min-w-0 flex-1"
319 value={value}
320 onCommit={(next) => props.onCommit(props.field, next)}
321 placeholder={props.field.placeholder}
322 configured={props.variable?.valueRedacted === true}
323 spellCheck={false}
324 aria-label={props.field.label}
325 sources={props.secretSources}
326 onSecretChange={(secret) => props.onSecretChange(props.field, secret)}
327 />
328 )}
329 {props.variable ? (
330 <Button
331 type="button"
332 size="icon-sm"
333 variant="ghost-destructive"
334 onClick={() => props.onRemove(props.field)}
335 aria-label={`Clear ${props.field.label}`}
336 >
337 <XIcon className="size-3.5" />
338 </Button>
339 ) : null}
340 </div>
341 }
342 />
343 );
345​
346export function ProviderEnvironmentSection(props: {
347 readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>;
348 readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void;
349 /** Variables an administrator's policy pins: listed by name, never editable or removable. */
350 readonly managed?: { readonly names: ReadonlySet<string>; readonly reason: string } | undefined;
351 readonly secretSources?: ReadonlyArray<SecretSource>;
352}) {
353 const [storedDraft, setDraft] = useState(() =>
354 seedRowsDraft(props.environment, toEnvironmentDraftRows),
355 );
356 let draft = reconcileRowsDraft(
357 storedDraft,
358 props.environment,
359 providerEnvironmentsEqual,
360 toEnvironmentDraftRows,
361 );
362 if (draft.rows !== storedDraft.rows) {
363 // Keep rows the server never acknowledged, even across an external reseed.
364 const acknowledged = new Set(storedDraft.seen.map((variable) => variable.name));
365 const unpublished = storedDraft.rows.filter(
366 (row) =>
367 row.id.startsWith(DRAFT_ROW_ID_PREFIX) &&
368 !acknowledged.has(row.name) &&
369 !draft.rows.some((incoming) => incoming.name === row.name),
370 );
371 if (unpublished.length > 0) draft = { ...draft, rows: [...draft.rows, ...unpublished] };
372 }
373 if (draft !== storedDraft) setDraft(draft);
374 const rows = draft.rows;
375 const setRows = (nextRows: ReadonlyArray<EnvironmentDraftRow>) =>
376 setDraft({ ...draft, rows: nextRows });
377​
378 const publishRows = (nextRows: ReadonlyArray<EnvironmentDraftRow>) => {
379 const published: ProviderInstanceEnvironmentVariable[] = [];
380 for (const row of nextRows) {
381 const name = row.name.trim();
382 if (!ENVIRONMENT_VARIABLE_NAME_PATTERN.test(name)) {
383 if (
384 name.length > 0 ||
385 row.value !== "" ||
386 row.sensitive !== true ||
387 row.valueRedacted !== undefined
388 ) {
389 return;
390 }
391 continue;
392 }
393 const { id: _id, ...rest } = row;
394 published.push({ ...rest, name });
395 }
396 setDraft({ rows: nextRows, seen: draft.seen, lastPublished: published });
397 props.onChange(published);
398 };
399​
400 const updateVariable = (id: string, patch: Partial<Omit<EnvironmentDraftRow, "id">>) => {
401 // A row named like a pinned variable renders as the policy's, with no way to edit or remove it.
402 if (patch.name !== undefined && props.managed?.names.has(patch.name)) return;
403 const nextRows = rows.map((row) =>
404 row.id === id
405 ? {
406 ...row,
407 ...patch,
408 ...(patch.value !== undefined ? { valueRedacted: false } : {}),
409 }
410 : row,
411 );
412 setRows(nextRows);
413 publishRows(nextRows);
414 };
415​
416 const setVariableSecret = (id: string, secret: SecretReference | undefined) => {
417 const nextRows = rows.map((row) => {
418 if (row.id !== id) return row;
419 const { valueRedacted: _valueRedacted, ...rest } = row;
420 return { ...rest, value: secret ?? "" };
421 });
422 setRows(nextRows);
423 publishRows(nextRows);
424 };
425​
426 const removeVariable = (id: string) => {
427 const nextRows = rows.filter((row) => row.id !== id);
428 setRows(nextRows);
429 publishRows(nextRows);
430 };
431​
432 const addVariable = () =>
433 setRows([
434 ...rows,
435 {
436 id: nextEnvironmentVariableDraftId(),
437 name: "",
438 value: "",
439 sensitive: true,
440 },
441 ]);
442​
443 return (
444 <SettingsRow
445 title="Variables"
446 description="API keys, base URLs, and other per-instance CLI settings."
447 control={
448 <Button type="button" size="sm" variant="secondary" onClick={addVariable}>
449 <PlusIcon className="size-3" />
450 Add variable
451 </Button>
452 }
453 >
454 {rows.length > 0 ? (
455 <div className="mt-3 min-w-0 space-y-2 pb-2">
456 {rows.map((variable, index) => {
457 if (props.managed?.names.has(variable.name)) {
458 return (
459 <div key={variable.id} className="flex min-w-0 flex-wrap items-center gap-1.5">
460 <code className="min-w-0 truncate text-meta sm:w-44 sm:shrink-0">
461 {variable.name}
462 </code>
463 <span className="text-meta text-muted-foreground">{props.managed.reason}</span>
464 </div>
465 );
466 }
467 const nameInput = (
468 <DraftInput
469 size="sm"
470 font="mono"
471 className="min-w-0 flex-1"
472 value={variable.name}
473 onCommit={(name) => updateVariable(variable.id, { name: name.trim() })}
474 disabled={variable.valueRedacted === true}
475 placeholder="VARIABLE_NAME"
476 spellCheck={false}
477 aria-label={`Environment variable name ${index + 1}`}
478 />
479 );
480 const removeButton = (
481 <Button
482 type="button"
483 size="icon-micro"
484 variant="ghost-destructive"
485 onClick={() => removeVariable(variable.id)}
486 aria-label={`Remove environment variable ${variable.name || index + 1}`}
487 >
488 <XIcon className="size-3" />
489 </Button>
490 );
491 const sensitiveToggle =
492 typeof variable.value === "string" ? (
493 <Tooltip>
494 <TooltipTrigger
495 render={
496 <Button
497 type="button"
498 size="icon-micro"
499 variant="ghost-muted"
500 disabled={variable.valueRedacted === true}
501 onClick={() => {
502 const sensitive = !variable.sensitive;
503 updateVariable(variable.id, {
504 sensitive,
505 ...(sensitive && variable.valueRedacted === undefined
506 ? {}
507 : { valueRedacted: sensitive ? variable.valueRedacted : false }),
508 });
509 }}
510 aria-pressed={variable.sensitive}
511 aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`}
512 >
513 {variable.sensitive ? (
514 <LockIcon className="size-3" />
515 ) : (
516 <LockOpenIcon className="size-3" />
517 )}
518 </Button>
519 }
520 />
521 <TooltipPopup side="top">
522 {variable.sensitive ? "Sensitive, stored separately" : "Plain text"}
523 </TooltipPopup>
524 </Tooltip>
525 ) : null;
526 const layout = (source: ReactNode, value: ReactNode) => (
527 <div className="flex min-w-0 flex-col gap-1.5 rounded-control surface-well p-2">
528 <div className="flex min-w-0 items-center gap-1.5">
529 {nameInput}
530 {source}
531 {removeButton}
532 </div>
533 <div className="flex min-w-0 items-start gap-1.5">
534 {value}
535 {sensitiveToggle}
536 </div>
537 </div>
538 );
539 return (
540 <Fragment key={variable.id}>
541 {variable.sensitive ? (
542 <SecretSourceInput
543 size="sm"
544 font="mono"
545 className="min-w-0 flex-1"
546 value={variable.valueRedacted ? "" : variable.value}
547 onCommit={(value) => updateVariable(variable.id, { value })}
548 placeholder="value"
549 configured={variable.valueRedacted === true}
550 spellCheck={false}
551 aria-label={`Environment variable value ${index + 1}`}
552 sources={props.secretSources ?? []}
553 onSecretChange={(secret) => setVariableSecret(variable.id, secret)}
554 >
555 {(parts) => layout(parts.source, parts.value)}
556 </SecretSourceInput>
557 ) : (
558 layout(
559 null,
560 <DraftInput
561 size="sm"
562 font="mono"
563 className="min-w-0 flex-1"
564 value={
565 variable.valueRedacted || typeof variable.value !== "string"
566 ? ""
567 : variable.value
568 }
569 onCommit={(value) => updateVariable(variable.id, { value })}
570 autoComplete="off"
571 placeholder="value"
572 spellCheck={false}
573 aria-label={`Environment variable value ${index + 1}`}
574 />,
575 )
576 )}
577 </Fragment>
578 );
579 })}
580 <p className="text-meta text-muted-foreground">
581 Sensitive values are stored separately and never returned to the app.
582 {rows.some((row) => row.valueRedacted === true)
583 ? " Enter a new value to rename a saved one or make it plain text."
584 : null}
585 </p>
586 </div>
587 ) : null}
588 </SettingsRow>
589 );
591​
592function ProjectOverrideInheritance({
593 projectEnabled,
594 deviceEnabled,
595}: {
596 readonly projectEnabled: boolean;
597 readonly deviceEnabled: boolean;
598}) {
599 const layers = [
600 { key: "project", label: "Project", enabled: projectEnabled, effective: true },
601 { key: "device", label: "Device", enabled: deviceEnabled, effective: false },
602 ];
603 return (
604 <Popover>
605 <Tooltip>
606 <TooltipTrigger
607 render={
608 <PopoverTrigger
609 render={
610 <Button
611 size="icon-micro"
612 variant="ghost-muted"
613 aria-label="Overridden for this project. Show where this value comes from"
614 />
615 }
616 />
617 }
618 >
619 <LayersIcon className="size-3 text-primary" />
620 </TooltipTrigger>
621 <TooltipPopup side="top">Overridden for this project</TooltipPopup>
622 </Tooltip>
623 <PopoverPopup align="end" width="md" padding="none">
624 <ol role="list" className="px-3 py-2.5 text-sm">
625 {layers.map((layer) => (
626 <li
627 key={layer.key}
628 className={cn(
629 "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 rounded-md px-2 py-1",
630 layer.effective && "bg-foreground/[0.06]",
631 )}
632 >
633 <span
634 className={cn(
635 "min-w-0 truncate",
636 layer.effective ? "font-medium text-foreground" : "text-muted-foreground",
637 )}
638 >
639 {layer.label}
640 </span>
641 <span
642 className={cn(
643 "flex items-center gap-1.5",
644 layer.effective ? "text-foreground" : "text-muted-foreground",
645 )}
646 >
647 {layer.enabled ? "On" : "Off"}
648 {layer.effective ? (
649 <CheckIcon aria-hidden className="size-3.5 shrink-0 text-primary" />
650 ) : (
651 <span aria-hidden className="size-3.5 shrink-0" />
652 )}
653 </span>
654 </li>
655 ))}
656 </ol>
657 </PopoverPopup>
658 </Popover>
659 );
661​
662interface ProjectOverrideControl {
663 readonly value: boolean | undefined;
664 readonly effectiveEnabled: boolean;
665 readonly onChange: (next: boolean) => void;
666 readonly onReset: () => void;
668​
669interface ProviderInstanceCardProps {
670 readonly instanceId: ProviderInstanceId;
671 readonly instance: ProviderInstanceConfig;
672 readonly driverOption: ProviderClientDefinition | undefined;
673 readonly liveProvider: ServerProvider | undefined;
674 /** "list" is the compact row in the providers list; "editor" is the selected instance's detail. */
675 readonly mode: "list" | "editor";
676 readonly selected?: boolean;
677 /** List cards open the full editor (a sheet) from the title or the menu. */
678 readonly onConfigure?: (() => void) | undefined;
679 /** List cards offer "Reset to defaults" for dirty built-in slots. */
680 readonly readOnly?: boolean | undefined;
681 readonly canWriteSettings?: boolean;
682 readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void;
683 readonly onDelete?: (() => void) | undefined;
684 readonly headerAction?: ReactNode | undefined;
685 readonly setup?: ReactNode;
686 readonly runtime?: ReactNode;
687 readonly hiddenModels: ReadonlyArray<string>;
688 readonly favoriteModels: ReadonlyArray<string>;
689 readonly modelOrder: ReadonlyArray<string>;
690 readonly onHiddenModelsChange: (
691 next: ReadonlyArray<string>,
692 affectedModels: ReadonlyArray<string>,
693 ) => void;
694 readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void;
695 readonly onToggleFavorite: (model: string) => void;
696 readonly onModelOrderChange: (next: ReadonlyArray<string>) => void;
697 readonly onRunUpdate?: (() => void) | undefined;
698 readonly onInstallRecommended?: (() => void) | undefined;
699 readonly isUpdating?: boolean | undefined;
700 readonly onAcceptUrlAuth?: ((action: AcpRegistryUrlAuthAction) => void) | undefined;
701 readonly environmentId?: EnvironmentId | undefined;
702 readonly secretSources?: ReadonlyArray<SecretSource>;
703 readonly projectOverride?: ProjectOverrideControl | undefined;
704 readonly acpProjects?:
705 | ReadonlyArray<{
706 readonly id: ProjectId;
707 readonly title: string;
708 readonly workspaceRoot: string;
709 }>
710 | undefined;
712​
713const EMPTY_ACP_PROJECTS: NonNullable<ProviderInstanceCardProps["acpProjects"]> = [];
714const NO_SECRET_SOURCES: ReadonlyArray<SecretSource> = [];
715const NO_CUSTOM_MODELS: ReadonlyArray<CustomModelDefinition> = [];
716​
717/** Unknown drivers stay read-only to preserve fork config. projectOverride switches affect only the
718 * project, not instance.enabled. */
719export function ProviderInstanceCard({
720 instanceId,
721 instance,
722 driverOption,
723 liveProvider,
724 mode,
725 selected = false,
726 onConfigure,
727 readOnly = false,
728 canWriteSettings = true,
729 onUpdate,
730 onDelete,
731 headerAction,
732 setup,
733 runtime,
734 hiddenModels,
735 favoriteModels,
736 modelOrder,
737 onHiddenModelsChange,
738 onFavoriteModelsChange,
739 onToggleFavorite,
740 onModelOrderChange,
741 onRunUpdate,
742 onInstallRecommended,
743 isUpdating = false,
744 onAcceptUrlAuth,
745 environmentId,
746 acpProjects = EMPTY_ACP_PROJECTS,
747 secretSources = NO_SECRET_SOURCES,
748 projectOverride,
749}: ProviderInstanceCardProps) {
750 const enabled = projectOverride?.effectiveEnabled ?? resolveProviderInstanceEnabled(instance);
751 const effectiveProvider = liveProvider && { ...liveProvider, enabled };
752 const compatibility = enabled ? liveProvider?.compatibilityAdvisory : undefined;
753 const statusKey = enabled ? getProviderStatusKey(effectiveProvider) : "disabled";
754 const statusStyle = PROVIDER_STATUS_STYLES[statusKey];
755 const summary = enabled
756 ? getProviderSummary(effectiveProvider)
757 : { headline: "Disabled", detail: null, authLabel: null };
758 const authEmail = liveProvider?.auth.email?.trim();
759 const isAuthenticated = enabled && liveProvider?.auth.status === "authenticated";
760 const authLabel =
761 enabled && liveProvider?.auth.status === "authenticated"
762 ? (liveProvider.auth.label ?? liveProvider.auth.type ?? null)
763 : null;
764 const versionLabel = getProviderVersionLabel(liveProvider?.version);
765 const versionAdvisory = getProviderVersionAdvisoryPresentation(
766 liveProvider?.versionAdvisory,
767 liveProvider?.compatibilityAdvisory,
768 enabled,
769 );
770 const updateCommand = versionAdvisory?.updateCommand ?? null;
771 const updateState = liveProvider?.updateState;
772 const updateProgress = isUpdating
773 ? ((updateState?.status === "queued" || updateState?.status === "running"
774 ? updateState.message
775 : null) ?? "Starting update")
776 : null;
777 const updateProblem =
778 !isUpdating && (updateState?.status === "failed" || updateState?.status === "unchanged")
779 ? updateState.message
780 : null;
781 const restartNotice =
782 enabled && !isUpdating && liveProvider ? getProviderRestartNotice(liveProvider) : null;
783 const hasCompatibilityWarning =
784 compatibility !== undefined &&
785 compatibility.status !== "supported" &&
786 compatibility.status !== "unknown";
787 const VersionAdvisoryIcon = hasCompatibilityWarning ? AlertTriangleIcon : ArrowUpCircleIcon;
788 // Version warnings never get a banner: the glyph carries the sentence and its popover the fix.
789 const versionAdvisoryLabel = versionAdvisory
790 ? `${versionAdvisory.title}. ${versionAdvisory.detail}`
791 : null;
792 const onRunVersionAction = readOnly
793 ? undefined
794 : versionAdvisory?.targetVersion
795 ? onInstallRecommended
796 : onRunUpdate;
797 const urlAuthAction = liveProvider?.auth.action;
798 const displayName =
799 instance.displayName?.trim() || driverOption?.label || String(instance.driver);
800 const accentColor = normalizeProviderAccentColor(instance.accentColor);
801 const managedPolicy = useManagedPolicy(environmentId);
802 const instanceLocks = managedInstanceLocks(managedPolicy, instance.driver);
803 const managedDriverReason = isManagedDriverAllowed(managedPolicy, instance.driver)
804 ? undefined
805 : disallowedProviderMessage(
806 managedPolicy?.organization,
807 driverOption?.label ?? String(instance.driver),
808 );
809 const { copyToClipboard } = useCopyToClipboard<{ providerName: string }>({
810 onCopy: ({ providerName }) => {
811 toastManager.add({
812 type: "success",
813 title: `${providerName} update command copied`,
814 description: "Run it in a terminal when you are ready to update.",
815 });
816 },
817 onError: (error, { providerName }) => {
818 toastManager.add(
819 stackedThreadToast({
820 type: "error",
821 title: `Could not copy ${providerName} update command`,
822 description: error.message,
823 }),
824 );
825 },
826 });
827​
828 const driverKind: ProviderDriverKind | null = isProviderDriverKind(instance.driver)
829 ? instance.driver
830 : null;
831 const storedCustomModels =
832 instance.config !== null && typeof instance.config === "object"
833 ? (instance.config as Record<string, unknown>).customModels
834 : undefined;
835 const customModels =
836 instance.driver === "antigravity"
837 ? NO_CUSTOM_MODELS
838 : readCustomModelEntries(storedCustomModels);
839 const modelsForDisplay = deriveProviderModelsForDisplay({
840 liveModels: liveProvider?.models,
841 customModels,
842 });
843 const updateDisplayName = (value: string) => {
844 const trimmed = value.trim();
845 const { displayName: _omit, ...rest } = instance;
846 onUpdate(
847 trimmed.length > 0
848 ? ({ ...rest, displayName: trimmed } as ProviderInstanceConfig)
849 : (rest as ProviderInstanceConfig),
850 );
851 };
852​
853 const updateEnabled = (value: boolean) => {
854 if (projectOverride) {
855 projectOverride.onChange(value);
856 return;
857 }
858 onUpdate({ ...instance, enabled: value });
859 };
860​
861 const updateAccentColor = (value: string) => {
862 const normalized = normalizeProviderAccentColor(value);
863 const { accentColor: _omit, ...rest } = instance;
864 onUpdate(
865 normalized
866 ? ({ ...rest, accentColor: normalized } as ProviderInstanceConfig)
867 : (rest as ProviderInstanceConfig),
868 );
869 };
870​
871 const updateConfig = (nextConfig: Record<string, unknown> | undefined) => {
872 const { config: _omit, ...rest } = instance;
873 onUpdate(
874 nextConfig !== undefined
875 ? ({ ...rest, config: nextConfig } as ProviderInstanceConfig)
876 : (rest as ProviderInstanceConfig),
877 );
878 };
879​
880 const updateCustomModels = useStableCallback((next: ReadonlyArray<CustomModelDefinition>) => {
881 const nextConfig = nextConfigBlobWithValue(
882 instance.config,
883 "customModels",
884 next.map(toCustomModelSetting),
885 );
886 const { config: _omit, ...rest } = instance;
887 onUpdate({ ...rest, config: nextConfig } as ProviderInstanceConfig);
888 });
889​
890 const updateEnvironment = (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => {
891 const cleaned = environment.filter((variable) => variable.name.trim().length > 0);
892 const { environment: _omit, ...rest } = instance;
893 onUpdate(
894 cleaned.length > 0
895 ? ({ ...rest, environment: cleaned } as ProviderInstanceConfig)
896 : (rest as ProviderInstanceConfig),
897 );
898 };
899 const environmentFields = driverOption?.environmentFields ?? [];
900 const environmentFieldNames = new Set(environmentFields.map((field) => field.name));
901 const genericEnvironment = providerEnvironmentWithoutNames(
902 instance.environment,
903 environmentFieldNames,
904 );
905 const updateGenericEnvironment = (
906 environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>,
907 ) => {
908 const dedicatedEnvironment = (instance.environment ?? []).filter((variable) =>
909 environmentFieldNames.has(variable.name),
910 );
911 updateEnvironment([...dedicatedEnvironment, ...environment]);
912 };
913 const updateEnvironmentField = (field: ProviderEnvironmentField, value: string) => {
914 updateEnvironment(nextProviderEnvironmentWithFieldValue(instance.environment, field, value));
915 };
916 const updateEnvironmentFieldSecret = (
917 field: ProviderEnvironmentField,
918 secret: SecretReference | undefined,
919 ) => {
920 updateEnvironment(nextProviderEnvironmentWithFieldSecret(instance.environment, field, secret));
921 };
922 const removeEnvironmentField = (field: ProviderEnvironmentField) => {
923 updateEnvironment(providerEnvironmentWithoutNames(instance.environment, new Set([field.name])));
924 };
925​
926 const titleIconNode = (
927 <ProviderInstanceIcon
928 driverKind={driverKind ?? instance.driver}
929 displayName={displayName}
930 accentColor={accentColor}
931 acpRegistryAgentId={
932 readConfigString(instance.config, "source") === "local"
933 ? undefined
934 : (readConfigString(instance.config, "agentId") ?? undefined)
935 }
936 acpRegistryIconUrl={
937 readConfigString(instance.config, "source") === "local"
938 ? undefined
939 : (readConfigString(instance.config, "registryIconUrl") ?? undefined)
940 }
941 showBadge={Boolean(accentColor)}
942 className="size-4"
943 iconClassName="size-4 text-muted-foreground"
944 badgeClassName="right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3 px-0.5 text-5xs"
945 />
946 );
947​
948 const titleTailNode = headerAction ? (
949 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">{headerAction}</span>
950 ) : null;
951​
952 const versionCodeNode = versionLabel ? (
953 <code className="font-mono text-meta text-dim-foreground">{versionLabel}</code>
954 ) : null;
955​
956 const statusDotNode =
957 statusKey === "warning" || statusKey === "error" ? <StatusDot tone={statusStyle.dot} /> : null;
958 const needsAttention = statusKey === "warning" || statusKey === "error";
959 const statusDiagnostic = (
960 <>
961 {authLabel || summary.authLabel ? (
962 <span className="block">{authLabel ?? summary.authLabel}</span>
963 ) : null}
964 {hasCompatibilityWarning && needsAttention && summary.detail ? (
965 <span className="block">{summary.detail}</span>
966 ) : null}
967 </>
968 );
969 const hasStatusDiagnostic = Boolean(
970 authLabel || summary.authLabel || (hasCompatibilityWarning && needsAttention && summary.detail),
971 );
972 const inlineStatusDetail = hasCompatibilityWarning
973 ? compatibility?.status === "broken"
974 ? "Incompatible"
975 : compatibility?.status === "unsupported"
976 ? "Unsupported"
977 : "Limited support"
978 : summary.detail;
979 const editorStatusNode =
980 isAuthenticated && authEmail ? (
981 <>
982 {needsAttention ? statusDotNode : null}
983 <span>Authenticated as</span>
984 <ProviderAuthEmail email={authEmail} />
985 {inlineStatusDetail ? (
986 <span className="min-w-0 basis-full [overflow-wrap:anywhere]">{inlineStatusDetail}</span>
987 ) : null}
988 </>
989 ) : (
990 <>
991 {statusDotNode}
992 <span>{summary.headline}</span>
993 {inlineStatusDetail ? (
994 <span className="min-w-0 basis-full [overflow-wrap:anywhere]">{inlineStatusDetail}</span>
995 ) : null}
996 </>
997 );
998 const versionAdvisoryNode = versionAdvisory ? (
999 <Popover>
1000 <Tooltip>
1001 <TooltipTrigger
1002 render={
1003 <PopoverTrigger
1004 render={
1005 <Button
1006 type="button"
1007 size={mode === "list" ? "icon-micro" : "icon-xs"}
1008 variant="ghost-muted"
1009 className={mode === "list" ? "pointer-events-auto relative shrink-0" : undefined}
1010 aria-label={`${updateProgress ? "Updating" : versionAdvisory.title}, view details`}
1012 {updateProgress ? (
1013 <Spinner tone="muted" {...(mode === "list" ? { size: "sm" as const } : {})} />
1014 ) : (
1015 <VersionAdvisoryIcon
1016 className={cn(
1017 mode === "list" && "size-3.5",
1018 hasCompatibilityWarning && "text-warning-foreground",
1019 )}
1020 />
1021 )}
1022 </Button>
1024 />
1026 />
1027 <TooltipPopup side="top">{updateProgress ? "Updating" : versionAdvisoryLabel}</TooltipPopup>
1028 </Tooltip>
1029 <PopoverPopup side="bottom" align="end" width="md" aria-label={versionAdvisory.title}>
1030 <div className="grid min-w-0 gap-3">
1031 <div className="grid gap-0.5">
1032 <p className="text-sm font-semibold leading-tight text-strong-foreground">
1033 {versionAdvisory.title}
1034 </p>
1035 <p
1036 className={cn(
1037 "text-xs leading-snug [overflow-wrap:anywhere]",
1038 versionAdvisory.emphasis === "strong"
1039 ? "text-warning-foreground"
1040 : "text-muted-foreground",
1041 )}
1043 {versionAdvisory.detail}
1044 </p>
1045 </div>
1046 {onRunVersionAction ? (
1047 <Button
1048 type="button"
1049 size="xs"
1050 variant="secondary"
1051 className="w-full"
1052 disabled={isUpdating}
1053 onClick={onRunVersionAction}
1055 {isUpdating ? <Spinner /> : <DownloadIcon />}
1056 {isUpdating
1057 ? "Updating"
1058 : versionAdvisory.targetVersion
1059 ? `Install ${getProviderVersionLabel(versionAdvisory.targetVersion)}`
1060 : "Update now"}
1061 </Button>
1062 ) : null}
1063 {updateProgress || updateProblem ? (
1064 <p
1065 aria-live="polite"
1066 className={cn(
1067 "text-xs leading-snug [overflow-wrap:anywhere]",
1068 updateProblem ? "text-warning" : "text-muted-foreground",
1069 )}
1071 {updateProgress ?? updateProblem}
1072 </p>
1073 ) : null}
1074 {onRunVersionAction && updateCommand ? (
1075 <div className="flex items-center gap-2 text-3xs font-medium text-muted-foreground">
1076 <span aria-hidden className="h-px flex-1 bg-border" />
1077 or, update manually using
1078 <span aria-hidden className="h-px flex-1 bg-border" />
1079 </div>
1080 ) : null}
1081 {updateCommand ? (
1082 <div className="flex min-w-0 items-center gap-1 rounded-control surface-well py-0.5 pr-0.5 pl-2">
1083 <code className="min-w-0 flex-1 truncate font-mono text-2xs text-foreground">
1084 {updateCommand}
1085 </code>
1086 <Tooltip>
1087 <TooltipTrigger
1088 render={
1089 <Button
1090 type="button"
1091 size="icon-xs"
1092 variant="ghost-muted"
1093 className="shrink-0"
1094 onClick={() => copyToClipboard(updateCommand, { providerName: displayName })}
1095 aria-label="Copy update command"
1097 <CopyIcon className="size-3" />
1098 </Button>
1100 />
1101 <TooltipPopup side="top">Copy command</TooltipPopup>
1102 </Tooltip>
1103 </div>
1104 ) : null}
1105 </div>
1106 </PopoverPopup>
1107 </Popover>
1108 ) : null;
1109​
1110 if (mode === "list") {
1111 const statusLine = updateProgress ? (
1112 <span aria-live="polite" className="line-clamp-2 [overflow-wrap:anywhere]">
1113 <span>Updating</span>
1114 <span className="block">{updateProgress}</span>
1115 </span>
1116 ) : isAuthenticated && authEmail ? (
1117 <>
1118 <span>Authenticated as</span>
1119 <ProviderAuthEmail email={authEmail} />
1120 </>
1121 ) : (
1122 <span className="line-clamp-2 [overflow-wrap:anywhere]">
1123 {enabled ? summary.headline : "Disabled"}
1124 {enabled && needsAttention && inlineStatusDetail ? (
1125 <span className="block">{inlineStatusDetail}</span>
1126 ) : null}
1127 </span>
1128 );
1129 return (
1130 <div
1131 data-slot="provider-card"
1132 data-selected={selected ? "" : undefined}
1133 className={cn(
1134 "relative flex items-start gap-2.5 rounded-row p-2.5",
1135 selected ? "surface-selected-soft" : "hover:surface-ghost-hover",
1136 )}
1138 {/* The select button sits under the text rather than around it, so the
1139 email, status and enable controls stay separate hit targets. */}
1140 <button
1141 type="button"
1142 className="absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
1143 onClick={onConfigure}
1144 aria-current={selected || undefined}
1145 aria-label={`Configure ${displayName}`}
1146 />
1147 <span
1148 className={cn(
1149 "pointer-events-none relative mt-px flex h-4.5 shrink-0 items-center",
1150 !enabled && "opacity-64",
1151 )}
1153 {titleIconNode}
1154 </span>
1155 <div
1156 className={cn("pointer-events-none relative min-w-0 flex-1", !enabled && "opacity-64")}
1158 {/* Wraps instead of shrinking, so the name keeps its width and the
1159 version and chips move to the next line in a narrow column. */}
1160 <span className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5">
1161 <Text variant="label" truncate>
1162 {displayName}
1163 </Text>
1164 {enabled && !updateProgress ? statusDotNode : null}
1165 {versionLabel ? (
1166 <code className="shrink-0 font-mono text-meta text-dim-foreground">
1167 {versionLabel}
1168 </code>
1169 ) : null}
1170 {enabled ? versionAdvisoryNode : null}
1171 {driverOption?.badgeLabel ? (
1172 <Badge variant="warning" size="sm">
1173 {driverOption.badgeLabel}
1174 </Badge>
1175 ) : null}
1176 </span>
1177 <ProviderStatusDiagnostic detail={hasStatusDiagnostic ? statusDiagnostic : null}>
1178 <span
1179 className={cn(
1180 "mt-0.5 flex min-w-0 flex-wrap items-baseline gap-x-1 text-meta leading-normal text-muted-foreground",
1181 hasStatusDiagnostic &&
1182 "pointer-events-auto rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
1183 )}
1184 tabIndex={hasStatusDiagnostic ? 0 : undefined}
1186 {statusLine}
1187 </span>
1188 </ProviderStatusDiagnostic>
1189 </div>
1190 <span className="relative flex shrink-0 items-center">
1191 <ManagedControl reason={enabled ? undefined : managedDriverReason}>
1192 <Switch
1193 checked={enabled}
1194 disabled={readOnly || (projectOverride !== undefined && !canWriteSettings)}
1195 onCheckedChange={(checked) => updateEnabled(Boolean(checked))}
1196 aria-label={`Enable ${displayName}`}
1197 />
1198 </ManagedControl>
1199 </span>
1200 </div>
1201 );
1203​
1204 const editorHeaderAction = (
1205 <div className="flex shrink-0 items-center gap-1.5">
1206 {driverOption?.badgeLabel ? (
1207 <Badge variant="warning" size="sm" className="shrink-0">
1208 {driverOption.badgeLabel}
1209 </Badge>
1210 ) : null}
1211 {versionCodeNode}
1212 {versionAdvisoryNode}
1213 <span
1214 inert={readOnly}
1215 aria-disabled={readOnly || undefined}
1216 // Empty, it would still add a flex gap and pull the version off the column edge.
1217 className={cn("inline-flex items-center gap-1 empty:hidden", readOnly && "opacity-50")}
1219 {titleTailNode}
1220 {projectOverride?.value !== undefined ? (
1221 <span className="flex items-center">
1222 <ProjectOverrideInheritance
1223 projectEnabled={projectOverride.value}
1224 deviceEnabled={resolveProviderInstanceEnabled(instance)}
1225 />
1226 <SettingResetButton
1227 label={`${displayName} enablement`}
1228 tooltip="Reset to the device setting"
1229 disabled={readOnly || !canWriteSettings}
1230 onClick={projectOverride.onReset}
1231 />
1232 </span>
1233 ) : null}
1234 {onDelete ? (
1235 <Button
1236 type="button"
1237 size="icon-xs"
1238 variant="ghost-destructive"
1239 disabled={readOnly}
1240 onClick={onDelete}
1241 aria-label={`Delete instance ${instanceId}`}
1243 <Trash2Icon />
1244 </Button>
1245 ) : null}
1246 </span>
1247 </div>
1248 );
1249​
1250 const statusContent = (
1251 <>
1252 <ProviderStatusDiagnostic detail={hasStatusDiagnostic ? statusDiagnostic : null}>
1253 <div
1254 tabIndex={hasStatusDiagnostic ? 0 : undefined}
1255 className="flex min-w-0 flex-wrap items-baseline gap-x-1.5"
1257 {editorStatusNode}
1258 </div>
1259 </ProviderStatusDiagnostic>
1260 {restartNotice ? (
1261 <p aria-live="polite" className="text-xs leading-snug [overflow-wrap:anywhere]">
1262 {restartNotice}
1263 </p>
1264 ) : null}
1265 {urlAuthAction && onAcceptUrlAuth ? (
1266 <div className="grid max-w-xl gap-1.5 pt-1 text-meta">
1267 <p>{urlAuthAction.message}</p>
1268 <code className="break-all text-caption">{urlAuthAction.url}</code>
1269 <Button
1270 render={
1271 <a
1272 href={urlAuthAction.url}
1273 target="_blank"
1274 rel="noopener noreferrer"
1275 onClick={() => onAcceptUrlAuth(urlAuthAction)}
1276 />
1278 size="xs"
1279 variant="secondary"
1280 className="w-fit"
1281 disabled={readOnly}
1283 <ExternalLinkIcon />
1284 Continue authentication
1285 </Button>
1286 </div>
1287 ) : null}
1288 </>
1289 );
1290​
1291 const runtimeFields = driverOption ? (
1292 <ProviderSettingsForm
1293 definition={driverOption}
1294 value={instance.config}
1295 idPrefix={`provider-instance-${instanceId}`}
1296 variant="settings"
1297 onChange={updateConfig}
1298 managed={instanceLocks && { keys: instanceLocks.configKeys, reason: instanceLocks.reason }}
1299 environmentId={environmentId}
1300 />
1301 ) : (
1302 <SettingsRow
1303 title="Driver"
1304 description={
1305 <span>
1306 This instance uses <code className="text-foreground">{String(instance.driver)}</code>,
1307 which is not available in this build. Its configuration is preserved.
1308 </span>
1310 />
1311 );
1312​
1313 return (
1314 <>
1315 <div className="space-y-4">
1316 <div className="flex min-h-7 items-center justify-between gap-2">
1317 <h2 className="flex min-w-0 items-center gap-2">
1318 {titleIconNode}
1319 <Text variant="label" truncate>
1320 {displayName}
1321 </Text>
1322 </h2>
1323 {editorHeaderAction}
1324 </div>
1325 <SettingsGroup>
1326 <SettingsRow
1327 title="Display name"
1328 status={statusContent}
1329 control={
1330 <div
1331 inert={readOnly}
1332 aria-disabled={readOnly || undefined}
1333 className={cn(
1334 "flex w-full min-w-0 items-center justify-end gap-2 @min-[26rem]/settings-row:w-auto",
1335 readOnly && "opacity-50 select-none",
1336 )}
1338 <ProviderAccentColorPicker
1339 layout="inline"
1340 displayName={displayName}
1341 value={accentColor}
1342 onCommit={updateAccentColor}
1343 />
1344 <DraftInput
1345 id={`provider-instance-${instanceId}-display-name`}
1346 size="settings"
1347 className="min-w-0 flex-1 @min-[26rem]/settings-row:w-50"
1348 value={instance.displayName ?? ""}
1349 onCommit={updateDisplayName}
1350 placeholder={driverOption?.label ?? "Instance label"}
1351 spellCheck={false}
1352 />
1353 </div>
1355 />
1356 </SettingsGroup>
1357 </div>
1358​
1359 {setup || environmentFields.length > 0 ? (
1360 <SettingsSection title="Setup">
1361 {setup}
1362 {environmentFields.length > 0 ? (
1363 <div
1364 inert={readOnly}
1365 aria-disabled={readOnly || undefined}
1366 className={readOnly ? "opacity-50 select-none" : undefined}
1368 {environmentFields.map((field) => (
1369 <ProviderEnvironmentFieldRow
1370 key={field.name}
1371 field={field}
1372 variable={readProviderEnvironmentVariable(instance.environment, field.name)}
1373 idPrefix={`provider-instance-${instanceId}`}
1374 onCommit={updateEnvironmentField}
1375 onSecretChange={updateEnvironmentFieldSecret}
1376 onRemove={removeEnvironmentField}
1377 managedReason={
1378 instanceLocks?.wholeEnvironment ||
1379 instanceLocks?.environmentNames.has(field.name)
1380 ? instanceLocks.reason
1381 : undefined
1383 secretSources={secretSources}
1384 />
1385 ))}
1386 </div>
1387 ) : null}
1388 </SettingsSection>
1389 ) : null}
1390​
1391 {instance.driver === "codex" && readCodexSetupMode(instance.config) === "managed" ? (
1392 <div
1393 inert={readOnly}
1394 aria-disabled={readOnly || undefined}
1395 className={readOnly ? "opacity-50 select-none" : undefined}
1397 <FoldedSettingsSection
1398 key={instanceId}
1399 id={`provider-instance-${instanceId}-runtime`}
1400 title="Runtime"
1401 headerPlacement="outside"
1403 {runtime ?? runtimeFields}
1404 </FoldedSettingsSection>
1405 </div>
1406 ) : !driverOption || deriveProviderSettingsFields(driverOption).length > 0 ? (
1407 <SettingsSection
1408 title="Runtime"
1409 inert={readOnly}
1410 aria-disabled={readOnly || undefined}
1411 className={readOnly ? "opacity-50 select-none" : undefined}
1413 {runtimeFields}
1414 </SettingsSection>
1415 ) : null}
1416​
1417 <SettingsSection
1418 title="Environment"
1419 inert={readOnly || instanceLocks?.wholeEnvironment}
1420 aria-disabled={readOnly || instanceLocks?.wholeEnvironment || undefined}
1421 className={readOnly ? "opacity-50 select-none" : undefined}
1423 {instanceLocks?.wholeEnvironment ? (
1424 <ManagedNotice>{instanceLocks.reason}</ManagedNotice>
1425 ) : null}
1426 <div className={instanceLocks?.wholeEnvironment ? "opacity-50" : undefined}>
1427 <ProviderEnvironmentSection
1428 environment={genericEnvironment}
1429 onChange={updateGenericEnvironment}
1430 secretSources={secretSources}
1431 managed={
1432 instanceLocks && {
1433 names: instanceLocks.environmentNames,
1434 reason: instanceLocks.reason,
1437 />
1438 </div>
1439 </SettingsSection>
1440 {environmentId !== undefined && liveProvider?.driver === "acpRegistry" ? (
1441 <AcpSessionManagementSection
1442 environmentId={environmentId}
1443 instanceId={instanceId}
1444 provider={liveProvider}
1445 projects={acpProjects}
1446 readOnly={readOnly}
1447 />
1448 ) : null}
1449​
1450 {driverOption !== undefined ? (
1451 <SettingsSection title="Models">
1452 <div className="px-3 py-3 sm:px-4">
1453 <p className="mb-3 text-meta text-muted-foreground">
1454 Model preferences and custom models are saved on the selected environment and shared
1455 across connected devices.
1456 </p>
1457 <ProviderModelsSection
1458 canManageCustomModels={!readOnly}
1459 canWritePreferences={canWriteSettings}
1460 instanceId={instanceId}
1461 driverKind={driverKind}
1462 models={modelsForDisplay}
1463 customModels={customModels}
1464 hiddenModels={hiddenModels}
1465 favoriteModels={favoriteModels}
1466 modelOrder={modelOrder}
1467 onChange={updateCustomModels}
1468 onHiddenModelsChange={onHiddenModelsChange}
1469 onFavoriteModelsChange={onFavoriteModelsChange}
1470 onToggleFavorite={onToggleFavorite}
1471 onModelOrderChange={onModelOrderChange}
1472 />
1473 </div>
1474 </SettingsSection>
1475 ) : null}
1476 </>
1477 );