Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ Makes local Markdown links in bb open directly in Moss, with bb's viewer kept as

Install: `bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/open-in-moss --yes`

### @Plugin

Adds installed and Community plugins to bb's existing `@` menu without installing or invoking them.

![Plugin mentions in bb](plugins/at-plugin/docs/screenshot.png)

[Source](plugins/at-plugin) · [README](plugins/at-plugin/README.md)

Install: `bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/at-plugin --yes`

### Timeline Comments

Attaches durable discussion threads to selected timeline text. Users and agents can reply, edit, resolve or reopen comments, review them together, and add open feedback to the composer for follow-up.
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions plugins/at-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# @Plugin

Adds installed and Community plugins to bb's existing `@` menu.

![Plugin mentions in bb](docs/screenshot.png)

## Install

```sh
bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/at-plugin --yes
```

## Use

Type `@`, search for a plugin, and select it.

- An installed plugin mention tells the agent which available plugin to prefer
when it is relevant.
- A Community plugin mention tells the agent that the plugin exists but must be
installed before it can be used.

A mention never installs, enables, configures, authenticates, or invokes a
plugin by itself.

## Develop

```sh
npm install
npm run check --workspace=bb-plugin-at-plugin
```
1 change: 1 addition & 0 deletions plugins/at-plugin/assets/at.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
176 changes: 176 additions & 0 deletions plugins/at-plugin/community-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { describe, expect, it } from "vitest";

import {
type CommunityCatalogRecord,
searchCommunityPlugins,
} from "./community-catalog";
import { decodeCommunityItemId, utf8ByteLength } from "./mention-context";

function community(
overrides: Partial<CommunityCatalogRecord> = {},
): CommunityCatalogRecord {
return {
author: { name: "Publisher", url: null },
category: "Developer tools",
compatible: true,
description: "Catalog description",
displayName: "Example",
entryId: "example-entry",
icon: null,
iconUrl: null,
incompatibleReason: null,
installed: false,
marketplace: "bb-community",
marketplaceDisplayName: "BB Community",
official: false,
pluginId: "example",
publisherKey: "publisher",
publisherLabel: "Publisher",
source: "git:https://example.test/plugin.git",
...overrides,
};
}

describe("Community eligibility", () => {
it("keeps only compatible, uninstalled bb-community entries", () => {
const entries = [
community({ pluginId: "valid", entryId: "valid", displayName: "Valid" }),
community({ pluginId: "installed", entryId: "installed", installed: true }),
community({ pluginId: "incompatible", entryId: "incompatible", compatible: false }),
community({ pluginId: "other-market", entryId: "other", marketplace: "acme" }),
];

expect(searchCommunityPlugins(entries, "").map((item) => item.title)).toEqual(["Valid"]);
});

it.each([
{ pluginId: " ", entryId: "entry", displayName: "Name" },
{ pluginId: "plugin", entryId: "\u0000\t", displayName: "Name" },
{ pluginId: "plugin", entryId: "entry", displayName: "\u0085 " },
{ pluginId: "界".repeat(200), entryId: "entry", displayName: "Name" },
])("rejects malformed normalized identity %#", (overrides) => {
expect(searchCommunityPlugins([community(overrides)], "")).toEqual([]);
});
});

describe("Community discovery", () => {
it("applies identity tiers and preserves host relevance within a tier", () => {
const entries = [
community({ pluginId: "prefix-b", entryId: "b", displayName: "Git Beta" }),
community({ pluginId: "substring", entryId: "s", displayName: "The Git Tool" }),
community({ pluginId: "git", entryId: "id-exact", displayName: "Zulu" }),
community({ pluginId: "prefix-a", entryId: "a", displayName: "Git Alpha" }),
community({ pluginId: "name-exact", entryId: "n", displayName: "Git" }),
];

expect(searchCommunityPlugins(entries, "git").map((item) => item.title)).toEqual([
"Zulu",
"Git",
"Git Beta",
"Git Alpha",
"The Git Tool",
]);
});

it("keeps catalog-only matches as a fallback instead of discarding them", () => {
const entry = community({
pluginId: "noema",
entryId: "noema",
displayName: "Noema",
description: "A memory system",
category: "Memory",
});

expect(searchCommunityPlugins([entry], "memory").map((item) => item.title)).toEqual([
"Noema",
]);
});

it("deduplicates stable plugin ids after ranking, keeping the better result", () => {
const entries = [
community({
pluginId: "duplicate",
entryId: "weak",
displayName: "The Same Helper",
description: "First host result",
}),
community({
pluginId: "duplicate",
entryId: "exact",
displayName: "Same",
description: "Better identity match",
}),
community({ pluginId: "other", entryId: "other", displayName: "Other Same" }),
];

const items = searchCommunityPlugins(entries, "same");
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({ title: "Same", subtitle: "Not installed · Better identity match" });
expect(decodeCommunityItemId(items[0]!.id).entryId).toBe("exact");
});

it("disambiguates duplicate normalized names and starts subtitles with Not installed", () => {
const entries = [
community({
pluginId: "one",
entryId: "one",
displayName: "Same Name",
description: "First",
}),
community({
pluginId: "two",
entryId: "two",
displayName: "same name",
description: "Second",
}),
];

const items = searchCommunityPlugins(entries, "");
expect(items.map((item) => item.subtitle)).toEqual([
"Not installed · one · First",
"Not installed · two · Second",
]);
});

it("uses the publisher label when the description is blank", () => {
const [item] = searchCommunityPlugins(
[community({ description: " \t", publisherLabel: " Acme\nLabs " })],
"",
);

expect(item?.subtitle).toBe("Not installed · Acme Labs");
});

it("sanitizes and bounds rows while preserving all opaque identity fields", () => {
const entry = community({
pluginId: "plug:in%一",
marketplace: "bb-community",
entryId: "entry:50%二",
displayName: `\u0000 Name\n${"😀".repeat(100)}`,
description: `\u0085Description\t${"界".repeat(200)}`,
});
const [item] = searchCommunityPlugins([entry], "");

expect(item?.title).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/u);
expect(item?.subtitle).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/u);
expect(utf8ByteLength(item?.title ?? "")).toBeLessThanOrEqual(120);
expect(utf8ByteLength(item?.subtitle ?? "")).toBeLessThanOrEqual(240);
expect(decodeCommunityItemId(item?.id ?? "")).toEqual({
pluginId: "plug:in%一",
marketplace: "bb-community",
entryId: "entry:50%二",
});
});

it("returns at most six rows", () => {
const entries = Array.from({ length: 9 }, (_, index) =>
community({
pluginId: `plugin-${index}`,
entryId: `entry-${index}`,
displayName: `Plugin ${index}`,
}),
);

expect(searchCommunityPlugins(entries, "")).toHaveLength(6);
});
});
131 changes: 131 additions & 0 deletions plugins/at-plugin/community-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { BbPluginApi, PluginMentionItem } from "@get-bb/plugin-sdk";

import {
MAX_ITEM_SUBTITLE_BYTES,
MAX_ITEM_TITLE_BYTES,
boundUntrustedText,
encodeCommunityItemId,
normalizeStableIdentity,
normalizeUntrustedText,
} from "./mention-context";

export type CommunityCatalogRecord = Awaited<
ReturnType<BbPluginApi["sdk"]["plugins"]["catalog"]["search"]>
>[number];

interface CommunityCandidate {
entry: CommunityCatalogRecord;
pluginId: string;
marketplace: string;
entryId: string;
displayName: string;
description: string;
publisherLabel: string;
normalizedName: string;
hostRank: number;
tier: number;
}

export const COMMUNITY_MARKETPLACE = "bb-community";
const RESULT_LIMIT = 6;

function folded(value: string): string {
return value.toLowerCase();
}

function identityMatchTier(
query: string,
displayName: string,
pluginId: string,
entryId: string,
): number {
const foldedQuery = folded(normalizeUntrustedText(query));
if (foldedQuery.length === 0) return 3;

const fields = [displayName, pluginId, entryId].map(folded);
if (fields.some((field) => field === foldedQuery)) return 0;
if (fields.some((field) => field.startsWith(foldedQuery))) return 1;
if (fields.some((field) => field.includes(foldedQuery))) return 2;
return 3;
}

function toCandidate(
entry: CommunityCatalogRecord,
query: string,
hostRank: number,
): CommunityCandidate | null {
if (
entry.marketplace !== COMMUNITY_MARKETPLACE ||
entry.installed !== false ||
entry.compatible !== true
) {
return null;
}

const pluginId = normalizeStableIdentity(entry.pluginId);
const entryId = normalizeStableIdentity(entry.entryId);
const displayName = normalizeUntrustedText(entry.displayName);
if (pluginId === null || entryId === null || displayName.length === 0) return null;

const description = normalizeUntrustedText(entry.description);
const publisherLabel = normalizeUntrustedText(entry.publisherLabel);
return {
entry,
pluginId,
marketplace: COMMUNITY_MARKETPLACE,
entryId,
displayName,
description,
publisherLabel,
normalizedName: folded(displayName),
hostRank,
tier: identityMatchTier(query, displayName, pluginId, entryId),
};
}

export function searchCommunityPlugins(
entries: readonly CommunityCatalogRecord[],
query: string,
): PluginMentionItem[] {
const ranked = entries
.map((entry, hostRank) => toCandidate(entry, query, hostRank))
.filter((candidate): candidate is CommunityCandidate => candidate !== null)
.sort((left, right) => left.tier - right.tier || left.hostRank - right.hostRank);

const seenPluginIds = new Set<string>();
const deduplicated = ranked.filter((candidate) => {
if (seenPluginIds.has(candidate.pluginId)) return false;
seenPluginIds.add(candidate.pluginId);
return true;
});

const duplicateNames = new Set(
Array.from(
deduplicated.reduce((counts, candidate) => {
counts.set(candidate.normalizedName, (counts.get(candidate.normalizedName) ?? 0) + 1);
return counts;
}, new Map<string, number>()),
)
.filter(([, count]) => count > 1)
.map(([name]) => name),
);

return deduplicated.slice(0, RESULT_LIMIT).map((candidate) => {
const detail = candidate.description || candidate.publisherLabel;
const subtitleParts = [
"Not installed",
...(duplicateNames.has(candidate.normalizedName) ? [candidate.pluginId] : []),
detail,
].filter(Boolean);

return {
id: encodeCommunityItemId({
pluginId: candidate.pluginId,
marketplace: candidate.marketplace,
entryId: candidate.entryId,
}),
title: boundUntrustedText(candidate.displayName, MAX_ITEM_TITLE_BYTES),
subtitle: boundUntrustedText(subtitleParts.join(" · "), MAX_ITEM_SUBTITLE_BYTES),
};
});
}
Loading
Loading