import type { Session, UpdateSessionToolPolicyRequest } from "@opengeni/sdk"; import type { CapabilityCatalogItem, ConnectionMetadata } from "../types"; import type { McpServerOption } from "./session-tools"; import { connectionHealth } from "./capabilities"; import { capabilityLogoSource } from "../components/capabilities/capability-logo-source"; /** Catalog identity, never a display-name match, joins selection to connection health. */ export function composerConnectorOptions( servers: readonly McpServerOption[], items: readonly CapabilityCatalogItem[], connections: ConnectionMetadata[] | null, assetUrl: (path: string | null) => string | null, ): McpServerOption[] { const byId = new Map(servers.map((server) => [server.id, server])); for (const item of items) { const id = item.runtime.mcpServerId; if (item.kind !== "mcp" || !item.enabled || !id) continue; const health = connectionHealth(item, connections ?? [], connections !== null); const state = health.state === "attention" ? !health.connection && item.connectionRef?.subjectScope === "subject" ? "connect" : health.connection?.status === "needs_reauth" ? "reconnect" : "unavailable" : health.state === "unverified" ? "unknown" : !item.runtime.available || item.lifecycle.readiness === "unavailable" ? "unavailable" : "ready"; const connection = health.state === "connected" || health.state === "attention" ? health.connection : null; byId.set(id, { id, name: item.name, logoSrc: capabilityLogoSource(item, assetUrl), connectionStatus: state, ...(connection ? { detail: connection.subjectId ? "Personal account" : "Workspace connection" } : {}), }); } return [...byId.values()]; } /** Only actual toggle changes alter exclusions; unavailable connections retain their choice. */ export function changedConnectorExclusions( previous: Iterable, before: ReadonlySet, after: ReadonlySet, ): string[] { const next = new Set(previous); for (const id of before) if (!after.has(id)) next.add(id); for (const id of after) if (!before.has(id)) next.delete(id); return [...next].sort(); } /** Reproject live defaults without turning a reconnect into a new user selection. */ export function defaultConnectorSelection( defaultIds: Iterable, excludedIds: Iterable, ): Set { const excluded = new Set(excludedIds); return new Set([...defaultIds].filter((id) => !excluded.has(id))); } /** A newly enabled connector outside defaults is an explicit session override. */ export function addsConnectorOutsideDefaults( before: ReadonlySet, after: ReadonlySet, defaultIds: Iterable, ): boolean { const defaults = new Set(defaultIds); return [...after].some((id) => !before.has(id) && !defaults.has(id)); } /** Connector switches change only connectors, preserving hidden refs and builtin choices. */ export function connectorSelectionUpdate( session: Pick, before: ReadonlySet, after: ReadonlySet, workspaceDefaultIds: Iterable, ): UpdateSessionToolPolicyRequest { if ( session.toolPolicy.mode === "workspace_default" && !addsConnectorOutsideDefaults(before, after, workspaceDefaultIds) ) { return { mode: "workspace_default", excludedMcpServerIds: changedConnectorExclusions( session.toolPolicy.excludedMcpServerIds ?? [], before, after, ), expectedVersion: session.toolPolicyVersion, }; } const removed = new Set([...before].filter((id) => !after.has(id))); const excluded = new Set( session.toolPolicy.mode === "workspace_default" ? session.toolPolicy.excludedMcpServerIds : [], ); const tools = session.tools.filter( (tool) => tool.id !== "opengeni" && !removed.has(tool.id) && (!excluded.has(tool.id) || after.has(tool.id)), ); const persistedIds = new Set(tools.map((tool) => tool.id)); for (const id of after) { if (!persistedIds.has(id)) tools.push({ kind: "mcp", id }); } return { mode: "explicit", tools, firstPartyMcpTools: [...session.firstPartyMcpTools], expectedVersion: session.toolPolicyVersion, }; } export function sessionConnectorPolicyIsCustomized(session: Pick): boolean { return ( session.toolPolicy.mode === "explicit" || (session.toolPolicy.excludedMcpServerIds?.length ?? 0) > 0 ); } export function followWorkspaceConnectorPolicy( session: Pick, ): UpdateSessionToolPolicyRequest { // Omit exclusions: supplying the field is a connector-only edit and 409s // while the session is still explicit. Omission fully adopts workspace defaults. return { mode: "workspace_default", expectedVersion: session.toolPolicyVersion, }; } export function newSessionConnectorCustomizeState(input: { toolsProvided: boolean; tools: ReadonlyArray<{ id: string }>; excludedMcpServerIds?: readonly string[] | null | undefined; }): { customizing: boolean; explicit: boolean } { const exclusions = input.excludedMcpServerIds ?? []; return { customizing: input.toolsProvided || exclusions.length > 0, explicit: input.toolsProvided && (input.tools.length > 0 || input.excludedMcpServerIds === undefined), }; }