diff --git a/apps/web/src/components/admin/CredentialBindingSelect.tsx b/apps/web/src/components/admin/CredentialBindingSelect.tsx new file mode 100644 index 0000000..111e0d3 --- /dev/null +++ b/apps/web/src/components/admin/CredentialBindingSelect.tsx @@ -0,0 +1,46 @@ +import type { Secret } from "../../lib/api-types" + +interface CredentialBindingSelectProps { + value: string + secrets: Secret[] + allowPersonal: boolean + allowCreateNew?: boolean + personalLabel: string + sharedLabel: string + personalPlaceholder?: string + createNewLabel?: string + onChange: (value: string) => void + className?: string +} + +/** Shared source selector used by Agent creation and Capability enabling. */ +export function CredentialBindingSelect({ + value, + secrets, + allowPersonal, + allowCreateNew = false, + personalLabel, + sharedLabel, + personalPlaceholder, + createNewLabel, + onChange, + className = "h-7 w-full rounded border border-line bg-surface px-2 text-sm", +}: CredentialBindingSelectProps) { + return ( + + ) +} diff --git a/apps/web/src/components/admin/CredentialCheckPanel.tsx b/apps/web/src/components/admin/CredentialCheckPanel.tsx index 271e457..05ead6d 100644 --- a/apps/web/src/components/admin/CredentialCheckPanel.tsx +++ b/apps/web/src/components/admin/CredentialCheckPanel.tsx @@ -4,6 +4,7 @@ import { Check, ChevronDown, ChevronRight, Eye, EyeOff, ExternalLink, Loader2, S import { Button } from "../ui/button" import { Input } from "../ui/input" +import { CredentialBindingSelect } from "./CredentialBindingSelect" import { useMyCredentials } from "../../lib/api-credentials" import { credentialKindLabel, @@ -12,12 +13,9 @@ import { type KnownCredentialKind, } from "../../lib/credential-kind-ui" import type { AgentInlineNewSecret, RequiredCredential, Secret } from "../../lib/api-types" +import { hasCredentialKind, sharedSecretsForKind, type PerKindBindingChoice } from "../../lib/credential-bindings" -/** PerKindBinding is the per-credential decision made in the picker. */ -export type PerKindBindingChoice = - | { source: "personal" } - | { source: "shared"; existing_secret_id: string } - | { source: "shared"; new_secret: { display_name: string; plaintext: string } } +export type { PerKindBindingChoice } from "../../lib/credential-bindings" interface CredentialCheckPanelProps { /** Only entries with required===true should be passed. */ @@ -171,7 +169,7 @@ export function CredentialCheckPanel({ for (const rc of requiredKinds) { const choice = choices[rc.kind] if (choice?.source !== "personal") continue - if (!(credentials ?? []).some((c) => c.kind === rc.kind)) { + if (!hasCredentialKind(credentials ?? [], rc.kind)) { // Personal but the creator has not configured this kind. Allow // the pick (other callers may have it), but signal invalid so // the create button stays disabled until they add it OR switch @@ -222,15 +220,8 @@ export function CredentialCheckPanel({ {requiredKinds.map((rc) => { const { displayName, placeholder, getUrl } = getKindMeta(rc.kind) const choice = choices[rc.kind] - const hasPersonalCredential = (credentials ?? []).some((c) => c.kind === rc.kind) - const kindSecrets = sharedSecrets.filter((s) => { - if (s.kind !== "capability_inline") return false - const metaCode = (s.metadata as { credential_kind_code?: unknown } | undefined)?.credential_kind_code - // Untagged legacy secrets surface for every kind (operator's - // responsibility to pick the right one); new secrets are - // always tagged so this only matters for pre-2026-06 rows. - return typeof metaCode !== "string" || metaCode === "" || metaCode === rc.kind - }) + const hasPersonalCredential = hasCredentialKind(credentials ?? [], rc.kind) + const kindSecrets = sharedSecretsForKind(sharedSecrets, rc.kind) return (
@@ -304,29 +295,29 @@ export function CredentialCheckPanel({ {choice?.source === "shared" && (
{kindSecrets.length > 0 && ( - + /> )} {kindSecrets.length === 0 && expandedNewSecretFor !== rc.kind && !("new_secret" in choice) && (