File size: 2,572 Bytes
9b906ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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));
}