import type { MarketplacePlugin } from "#/api/plugins-service"; import type { PluginSpec } from "#/api/conversation-service/agent-server-conversation-service.types"; /** * Stable identity for a plugin reference. Two references are "the same" when * their attachable coordinates (source / ref / repo_path) match — `name`, * `description`, install state, and parameters are intentionally excluded, * since the start-conversation payload only carries coordinates. * * `null` and `undefined` collapse to "" so a catalog entry (`ref?: null`) and a * stored spec (`ref: null`) compare equal. */ export function pluginSpecKey( plugin: Pick, ): string { return [plugin.source, plugin.ref ?? "", plugin.repo_path ?? ""].join(""); } /** * Map a marketplace catalog entry to the attachable `PluginSpec` consumed by * conversation creation. `parameters` is intentionally omitted (out of scope — * tracked under the plugin-parameters work) and the agent-server adapter drops * it from the payload regardless. */ export function marketplacePluginToSpec(plugin: MarketplacePlugin): PluginSpec { return { source: plugin.source, ref: plugin.ref ?? null, repo_path: plugin.repo_path ?? null, }; } /** Whether `plugin`'s coordinates are present in the current selection. */ export function isPluginSelected( selected: PluginSpec[], plugin: Pick, ): boolean { const key = pluginSpecKey(plugin); return selected.some((spec) => pluginSpecKey(spec) === key); } /** * Immutably add or remove a catalog plugin from the selection, de-duplicated by * coordinate key. Returns a new array. */ export function togglePluginSelection( selected: PluginSpec[], plugin: MarketplacePlugin, ): PluginSpec[] { const key = pluginSpecKey(plugin); if (selected.some((spec) => pluginSpecKey(spec) === key)) { return selected.filter((spec) => pluginSpecKey(spec) !== key); } return [...selected, marketplacePluginToSpec(plugin)]; } /** Case-insensitive match over a catalog entry's user-visible text. */ export function matchesPluginPickerSearch( plugin: MarketplacePlugin, query: string, ): boolean { const trimmed = query.trim().toLowerCase(); if (!trimmed) return true; return [ plugin.name, plugin.description, plugin.source, plugin.repo_path, plugin.ref, ] .filter( (value): value is string => typeof value === "string" && value.length > 0, ) .some((field) => field.toLowerCase().includes(trimmed)); }