openhands / src /components /features /plugins /plugin-spec-identity.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
9b906ea verified
Raw
History Blame Contribute Delete
2.57 kB
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<PluginSpec, "source" | "ref" | "repo_path">,
): 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<PluginSpec, "source" | "ref" | "repo_path">,
): 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));
}