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

ProviderInstanceCard.tsx

1,431 lines50.8 KB

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