Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
## Plugin API

- Any new public plugin API member (a `@get-bb/plugin-sdk/app` export, an `app.slots.*` method, or a `BbPluginApi` property) ships with an `experimental_` name prefix and an entry in [docs/api_to_audit.md](docs/api_to_audit.md) describing what it does and what to audit before stabilizing. Dropping the prefix is the deliberate stabilization step: audit the entry, rename project-wide, and remove it from the doc in the same change.
- The bb Plugin Guide (the `plugin-api-docs` plugin, rendering `packages/plugin-api-map`) is bb's only plugin API documentation. A new surface needs a card in `packages/plugin-api-map/src/surfaces.ts` naming its SDK symbols in the same change; `packages/plugin-api-map/test/api-sync.test.ts` fails the build when the map and the SDK drift apart.

## Data Access

Expand Down
5 changes: 0 additions & 5 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import {
SETTINGS_SECTION_ROUTE_PATH,
SKILLS_ROUTE_PATH,
TOOLS_PLUGIN_BROWSE_ROUTE_PATH,
TOOLS_PLUGIN_DETAIL_ROUTE_PATH,
TOOLS_PLUGINS_ROUTE_PATH,
TOOLS_REGISTRY_SKILL_DETAIL_ROUTE_PATH,
TOOLS_REGISTRY_SKILLS_ROUTE_PATH,
Expand Down Expand Up @@ -319,10 +318,6 @@ function AppRoutes() {
path={TOOLS_PLUGIN_BROWSE_ROUTE_PATH}
element={<ExtensionsLandingRedirect />}
/>
<Route
path={TOOLS_PLUGIN_DETAIL_ROUTE_PATH}
element={<ToolsView />}
/>
<Route
path={LEGACY_SKILLS_ROUTE_PATH}
element={<Navigate to={SKILLS_ROUTE_PATH} replace />}
Expand Down
23 changes: 23 additions & 0 deletions apps/app/src/components/plugin/PluginComposerBanners.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,27 @@ describe("ComposerBannersSlot", () => {
);
expect(view.container.textContent).toBe("Plugin rowBB row");
});

it("preserves BB-owned rows when plugin contributions are excluded", () => {
setPluginSlotRegistrations(
"excluded-plugin",
registrations([
{
id: "excluded",
banners: [{ id: "plugin", component: () => <div>Plugin row</div> }],
},
]),
);

const view = render(
<ComposerBannersSlot
view={composerView("t1")}
includePluginContributions={false}
>
<div>BB row</div>
</ComposerBannersSlot>,
);

expect(view.container.textContent).toBe("BB row");
});
});
16 changes: 13 additions & 3 deletions apps/app/src/components/plugin/PluginComposerBanners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,26 @@ export function ComposerBannersSlot({
view,
children,
ownerPlacement = "after",
includePluginContributions = true,
}: {
view?: ComposerView;
children?: ReactNode;
ownerPlacement?: "before" | "after";
includePluginContributions?: boolean;
}) {
return view === undefined ? (
<ComposerBannerRows ownerPlacement={ownerPlacement}>
<ComposerBannerRows
ownerPlacement={ownerPlacement}
includePluginContributions={includePluginContributions}
>
{children}
</ComposerBannerRows>
) : (
<PluginComposerViewProvider value={view}>
<ComposerBannerRows ownerPlacement={ownerPlacement}>
<ComposerBannerRows
ownerPlacement={ownerPlacement}
includePluginContributions={includePluginContributions}
>
{children}
</ComposerBannerRows>
</PluginComposerViewProvider>
Expand All @@ -35,15 +43,17 @@ export function ComposerBannersSlot({
function ComposerBannerRows({
children,
ownerPlacement,
includePluginContributions,
}: {
children?: ReactNode;
ownerPlacement: "before" | "after";
includePluginContributions: boolean;
}) {
const view = useOptionalPluginComposerView();
const banners = useResolvedComposerBanners(view?.scope.kind ?? null);
const scopeKey =
view === undefined ? null : composerScopeIdentity(view.scope);
const pluginRows = banners.map(
const pluginRows = (includePluginContributions ? banners : []).map(
({ key, pluginId, customizationId, banner }) => {
const slotId = `${customizationId}/${banner.id}`;
return (
Expand Down
32 changes: 32 additions & 0 deletions apps/app/src/components/plugin/PluginNewThreadComposer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,38 @@ describe("PluginNewThreadComposer seeding", () => {
});
});

it("maps the public customization policy to host composer isolation", async () => {
const composer = (policy?: "all" | "none") => (
<MemoryRouter>
<PluginNewThreadComposer
draftKey="plugin-customization-policy"
defaultProjectId="proj_1"
experimental_pluginCustomizations={policy}
onSubmit={() => undefined}
/>
</MemoryRouter>
);
const view = render(composer());

expect(latestPromptBoxProps().suppressPluginComposerCustomizations).toBe(
false,
);

view.rerender(composer("none"));
await waitFor(() => {
expect(latestPromptBoxProps().suppressPluginComposerCustomizations).toBe(
true,
);
});

view.rerender(composer("all"));
await waitFor(() => {
expect(latestPromptBoxProps().suppressPluginComposerCustomizations).toBe(
false,
);
});
});

it("binds plugin draft actions to the hosted composer instance", async () => {
renderComposer(STORED_REQUEST, () => undefined, "host-binding");

Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/components/plugin/PluginNewThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function PluginNewThreadComposer({
focusRequest,
className,
draftKey,
experimental_pluginCustomizations,
onSubmit,
}: PluginComposerProps) {
const pluginId = useContext(PluginContext);
Expand Down Expand Up @@ -79,6 +80,8 @@ export function PluginNewThreadComposer({
{renderPromptBox({
placeholder,
allowNoProject: true,
suppressPluginComposerCustomizations:
experimental_pluginCustomizations === "none",
})}
</div>
)}
Expand Down
8 changes: 8 additions & 0 deletions apps/app/src/components/plugin/PluginSlotMount.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
import { Pill } from "@bb/shared-ui/pill";
import { useRouteAnchorDelegate } from "@/components/ui/app-route-anchor";
import { usePluginCss } from "@/lib/plugin-css";
import {
PluginContext,
Expand Down Expand Up @@ -224,6 +225,7 @@ export function PluginSlotMount({
instanceId,
onCrash,
}: PluginSlotMountProps) {
const onRouteAnchorClick = useRouteAnchorDelegate();
usePluginCss(pluginId);
return (
<PluginContext.Provider value={pluginId}>
Expand All @@ -242,6 +244,12 @@ export function PluginSlotMount({
data-bb-plugin-root=""
data-bb-plugin={pluginId}
className="contents"
// Links in plugin UI behave like links anywhere else in bb: a plain
// click on an app route navigates client-side (a bare anchor would
// full-load the app and wipe its Back history), and cmd-click opens
// a splittable page beside the focused pane. Bubble phase, so the
// plugin's own handlers run first and can preventDefault.
onClick={onRouteAnchorClick}
>
{children}
</div>
Expand Down
Loading
Loading