From 3f7e5423b3ccde3caf71df8e51ca272b53f9a39c Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Mon, 3 Aug 2026 18:02:23 -0700 Subject: [PATCH 1/2] Polish extension provenance and settings --- .../PluginSidebarFooterActions.test.tsx | 11 ++-- .../plugin/PluginSidebarFooterActions.tsx | 16 +----- .../management/BrowsePluginsTab.test.tsx | 4 ++ .../plugin/management/BrowsePluginsTab.tsx | 40 ++++--------- .../PluginSettingsCompatibilityRoute.test.tsx | 47 ++-------------- .../PluginSettingsCompatibilityRoute.tsx | 27 +-------- .../components/settings/settings-nav.test.tsx | 51 +---------------- .../src/components/settings/settings-nav.tsx | 20 ++----- .../components/tools/PluginCapabilities.tsx | 56 +------------------ .../app/src/components/tools/PluginDetail.tsx | 14 +---- .../src/components/tools/SkillsCollection.tsx | 22 ++++++-- .../tools/detail-page-recipes.test.tsx | 33 ++++++++++- .../components/tools/plugin-detail-table.tsx | 25 ++++++++- apps/app/src/views/SkillsView.test.tsx | 11 +++- .../views/ToolsView.plugin-detail.test.tsx | 21 +------ .../src/components/ui/resource/toolbar.tsx | 21 +++++-- 16 files changed, 139 insertions(+), 280 deletions(-) diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx index 19eae7b26..9ba811411 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx @@ -118,12 +118,9 @@ describe("PluginSidebarFooterActions", () => { ); }); - it.each([ - [false, "/settings/plugins/remote"], - [true, "/tools/plugins/remote"], - ] as const)( - "opens the %s Tools Hub plugin settings destination", - (toolsHubEnabled, expectedPath) => { + it.each([false, true] as const)( + "opens Settings with Tools Hub set to %s", + (toolsHubEnabled) => { setPluginSlotRegistrations( "remote", registrationSet({ @@ -142,7 +139,7 @@ describe("PluginSidebarFooterActions", () => { fireEvent.click(screen.getByRole("button", { name: "Remote settings" })); expect(screen.getByLabelText("Current path").textContent).toBe( - expectedPath, + "/settings/plugins/remote", ); }, ); diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx index ecc4fe076..49e253a95 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx @@ -7,11 +7,7 @@ import { usePluginSlots, type PluginSidebarFooterActionSlot, } from "@/lib/plugin-slots"; -import { useToolsHubExperiment } from "@/components/tools/tools-experiment-context"; -import { - getPluginDetailRoutePath, - getSettingsPluginRoutePath, -} from "@/lib/route-paths"; +import { getSettingsPluginRoutePath } from "@/lib/route-paths"; const SIDEBAR_FOOTER_ACTION_CLASS = cn( COARSE_POINTER_CHILD_ICON_BUTTON_CLASS, @@ -43,7 +39,6 @@ function PluginSidebarFooterActionList({ onNavigate?: () => void; }) { const navigate = useNavigate(); - const toolsHubEnabled = useToolsHubExperiment(); return ( <> {actions.map((action) => ( @@ -65,7 +60,6 @@ function PluginSidebarFooterActionList({ runSidebarFooterAction({ action, navigate, - toolsHubEnabled, }); }} > @@ -81,18 +75,12 @@ function PluginSidebarFooterActionList({ function runSidebarFooterAction({ action, navigate, - toolsHubEnabled, }: { action: PluginSidebarFooterActionSlot; navigate: ReturnType; - toolsHubEnabled: boolean; }): void { const openSettings = () => { - void navigate( - toolsHubEnabled - ? getPluginDetailRoutePath({ pluginId: action.pluginId }) - : getSettingsPluginRoutePath(action.pluginId), - ); + void navigate(getSettingsPluginRoutePath(action.pluginId)); }; const warn = (error: unknown) => { console.warn( diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx index 32f16ab51..5c10b8868 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx @@ -124,6 +124,10 @@ describe("BrowsePluginsTab", () => { .getByRole("button", { name: "Open GitHub details" }) .closest('[class*="auto-fill"]'); expect(githubGrid?.className).toContain("auto-fill"); + expect(screen.queryByRole("heading", { name: "Productivity" })).toBeNull(); + expect( + screen.queryByRole("heading", { name: "Developer tools" }), + ).toBeNull(); expect(screen.queryByText(MEMORY_ENTRY.source)).toBeNull(); expect(screen.getByText("Requires a newer BB version")).toBeTruthy(); diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx index 9022eb2a1..1b535a69e 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx @@ -53,20 +53,13 @@ export function BrowsePluginsTab({ resetKey: debouncedQuery.toLowerCase(), }); - const byCategory = new Map(); - for (const entry of pagination.items) { - const bucket = byCategory.get(entry.category); - if (bucket === undefined) byCategory.set(entry.category, [entry]); - else bucket.push(entry); - } - return ( -
+

BB Official plugins

@@ -130,28 +123,17 @@ export function BrowsePluginsTab({ } /> ) : ( -
- {[...byCategory.entries()].map(([category, categoryEntries]) => ( -
-

- {category} -

- - {categoryEntries.map((entry) => ( - - ))} - -
+ + {pagination.items.map((entry) => ( + ))} -
+ )} ); diff --git a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx index ab287f3e8..40ade07a4 100644 --- a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx +++ b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx @@ -1,29 +1,11 @@ // @vitest-environment jsdom import { cleanup, render, screen } from "@testing-library/react"; -import { defaultExperiments } from "@bb/domain"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { PluginSettingsCompatibilityRoute } from "./PluginSettingsCompatibilityRoute"; -const mocks = vi.hoisted(() => ({ - useSystemConfig: vi.fn(), -})); - -vi.mock("@/hooks/queries/system-queries", () => ({ - useSystemConfig: mocks.useSystemConfig, -})); - -function renderRoute(path: string, toolsHub: boolean | undefined) { - mocks.useSystemConfig.mockReturnValue({ - data: - toolsHub === undefined - ? undefined - : { - experiments: { ...defaultExperiments, toolsHub }, - }, - }); - +function renderRoute(path: string) { render( @@ -54,37 +36,20 @@ function renderRoute(path: string, toolsHub: boolean | undefined) { } describe("PluginSettingsCompatibilityRoute", () => { - beforeEach(() => { - mocks.useSystemConfig.mockReset(); - }); - afterEach(cleanup); - it("keeps the existing Settings manager available while Tools Hub is off", () => { - renderRoute("/settings/plugins", false); + it("keeps the Settings manager available", () => { + renderRoute("/settings/plugins"); expect(screen.getByText("Settings plugin manager")).toBeTruthy(); expect(screen.queryByText("Tools plugins")).toBeNull(); }); - it("keeps existing Settings plugin detail routes available while Tools Hub is off", () => { - renderRoute("/settings/plugins/example", false); + it("keeps Settings plugin detail routes available", () => { + renderRoute("/settings/plugins/example"); expect(screen.getByText("Settings plugin detail")).toBeTruthy(); expect(screen.queryByText("Tools plugin detail")).toBeNull(); }); - it("redirects legacy Settings plugin routes to Tools Hub when enabled", () => { - renderRoute("/settings/plugins/example", true); - - expect(screen.getByText("Tools plugin detail")).toBeTruthy(); - expect(screen.queryByText("Settings plugin detail")).toBeNull(); - }); - - it("renders neither management surface while configuration is loading", () => { - renderRoute("/settings/plugins", undefined); - - expect(screen.queryByText("Settings plugin manager")).toBeNull(); - expect(screen.queryByText("Tools plugins")).toBeNull(); - }); }); diff --git a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx index 2092476a9..0aca2bcd9 100644 --- a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx +++ b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx @@ -1,32 +1,9 @@ import type { ReactNode } from "react"; -import { Navigate, useParams } from "react-router-dom"; -import { useSystemConfig } from "@/hooks/queries/system-queries"; -import { - getPluginDetailRoutePath, - getPluginsRoutePath, -} from "@/lib/route-paths"; - -/** Keeps the existing Settings manager available while Tools Hub is off. */ +/** Plugin configuration always belongs to Settings, independent of Tools Hub. */ export function PluginSettingsCompatibilityRoute({ children, }: { children: ReactNode; }) { - const { pluginId } = useParams<{ pluginId?: string }>(); - const systemConfig = useSystemConfig(); - const toolsHubEnabled = systemConfig.data?.experiments.toolsHub; - - if (toolsHubEnabled === undefined) return null; - if (!toolsHubEnabled) return children; - - return ( - - ); + return children; } diff --git a/apps/app/src/components/settings/settings-nav.test.tsx b/apps/app/src/components/settings/settings-nav.test.tsx index e52e4a655..eefbec1ab 100644 --- a/apps/app/src/components/settings/settings-nav.test.tsx +++ b/apps/app/src/components/settings/settings-nav.test.tsx @@ -3,20 +3,11 @@ import { cleanup, renderHook } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import type { ReactNode } from "react"; -import { defaultExperiments } from "@bb/domain"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { resetPluginSlotStoreForTest } from "@/lib/plugin-slots"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; import { useSettingsNavState } from "./settings-nav"; -const mocks = vi.hoisted(() => ({ - useSystemConfig: vi.fn(), -})); - -vi.mock("@/hooks/queries/system-queries", () => ({ - useSystemConfig: mocks.useSystemConfig, -})); - vi.mock("@/hooks/queries/plugin-settings-queries", () => ({ usePluginList: () => ({ data: { plugins: [] } }), })); @@ -42,18 +33,6 @@ afterEach(() => { vi.clearAllMocks(); }); -beforeEach(() => { - mocks.useSystemConfig.mockReset(); - mocks.useSystemConfig.mockReturnValue({ - data: { - experiments: { - ...defaultExperiments, - toolsHub: false, - }, - }, - }); -}); - describe("useSettingsNavState", () => { it("resolves Codex and Claude Code as separate provider pages", () => { const { result } = renderHook(() => useSettingsNavState(), { @@ -88,15 +67,7 @@ describe("useSettingsNavState", () => { ); }); - it("keeps plugin management in Settings while Tools Hub is disabled", () => { - mocks.useSystemConfig.mockReturnValue({ - data: { - experiments: { - ...defaultExperiments, - toolsHub: false, - }, - }, - }); + it("keeps plugin management in Settings", () => { const { result } = renderHook(() => useSettingsNavState(), { wrapper: wrapperFor("/settings"), }); @@ -106,22 +77,4 @@ describe("useSettingsNavState", () => { ); }); - it("removes plugin management from Settings while Tools Hub is enabled", () => { - mocks.useSystemConfig.mockReturnValue({ - data: { - experiments: { - ...defaultExperiments, - toolsHub: true, - }, - }, - }); - const { result } = renderHook(() => useSettingsNavState(), { - wrapper: wrapperFor("/settings"), - }); - - expect(result.current.sections.map((section) => section.id)).not.toContain( - "plugins", - ); - expect(result.current.pluginEntries).toEqual([]); - }); }); diff --git a/apps/app/src/components/settings/settings-nav.tsx b/apps/app/src/components/settings/settings-nav.tsx index b43d7255f..b6b871ef7 100644 --- a/apps/app/src/components/settings/settings-nav.tsx +++ b/apps/app/src/components/settings/settings-nav.tsx @@ -4,7 +4,6 @@ import { usePluginList, type PluginListItem, } from "@/hooks/queries/plugin-settings-queries"; -import { useSystemConfig } from "@/hooks/queries/system-queries"; import { useHostDaemon } from "@/hooks/useHostDaemon"; import { usePluginSlots } from "@/lib/plugin-slots"; import { PluginIcon } from "@/components/plugin/PluginIcon"; @@ -84,12 +83,10 @@ export function useSettingsNavState(): SettingsNavState { const location = useLocation(); const { hasDaemon } = useHostDaemon(); const { fileOpeners, settingsSections } = usePluginSlots(); - const systemConfig = useSystemConfig(); - const toolsHubEnabled = systemConfig.data?.experiments.toolsHub === true; const settingsSectionPluginIds = new Set( settingsSections.map((section) => section.pluginId), ); - const pluginListQuery = usePluginList({ enabled: !toolsHubEnabled }); + const pluginListQuery = usePluginList({ enabled: true }); const pluginMatch = matchPath(SETTINGS_PLUGIN_ROUTE_PATH, location.pathname); const providerMatch = matchPath( @@ -132,18 +129,13 @@ export function useSettingsNavState(): SettingsNavState { if (section.id === "files") { return hasDaemon || fileOpeners.length > 0; } - if (section.id === "plugins") { - return !toolsHubEnabled; - } return true; }); - const pluginEntries = toolsHubEnabled - ? [] - : (pluginListQuery.data?.plugins ?? []).filter( - (plugin) => - plugin.enabled && - (plugin.hasSettings || settingsSectionPluginIds.has(plugin.id)), - ); + const pluginEntries = (pluginListQuery.data?.plugins ?? []).filter( + (plugin) => + plugin.enabled && + (plugin.hasSettings || settingsSectionPluginIds.has(plugin.id)), + ); return { activeMachineId, activePluginId, diff --git a/apps/app/src/components/tools/PluginCapabilities.tsx b/apps/app/src/components/tools/PluginCapabilities.tsx index 14a0e6695..5ebdc3cbc 100644 --- a/apps/app/src/components/tools/PluginCapabilities.tsx +++ b/apps/app/src/components/tools/PluginCapabilities.tsx @@ -22,7 +22,6 @@ import { appToast } from "@/components/ui/app-toast"; import { invalidatePluginList } from "@/hooks/cache-owners/plugin-cache-owner"; import { reloadPlugin, - usePluginSettingsView, type PluginListItem, } from "@/hooks/queries/plugin-settings-queries"; import { usePluginSlots, type PluginSlotSnapshot } from "@/lib/plugin-slots"; @@ -142,15 +141,6 @@ interface PluginCapabilityItem { mono?: boolean; } -function capabilityDetail(kind: string, id?: string): ReactNode { - return ( - - {kind} - {id ? {id} : null} - - ); -} - function namedSurface( prefix: string, id: string, @@ -286,24 +276,12 @@ function pluginAppSurfaceItems( export function PluginIncludes({ plugin, - hasSettings, }: { plugin: PluginListItem; - hasSettings: boolean; }) { const slots = usePluginSlots(); - const settingsQuery = usePluginSettingsView(plugin.id, { - enabled: plugin.hasSettings, - }); - const settingsSections = slots.settingsSections.filter( - (slot) => slot.pluginId === plugin.id, - ); const appItems = pluginAppSurfaceItems(plugin.id, slots); - if ( - plugin.app.hasApp && - appItems.length === 0 && - settingsSections.length === 0 - ) { + if (plugin.app.hasApp && appItems.length === 0) { appItems.push({ key: "frontend-app", label: "Frontend app", @@ -311,33 +289,6 @@ export function PluginIncludes({ }); } - const settingsItems: PluginCapabilityItem[] = [ - ...Object.entries(settingsQuery.data?.schema ?? {}).map( - ([key, descriptor]) => ({ - key: `setting:${key}`, - label: descriptor.label, - detail: capabilityDetail("Setting", key), - }), - ), - ...settingsSections.map((slot) => - namedSurface( - "settings-section", - slot.id, - slot.title, - "Custom settings section", - ), - ), - ]; - if (hasSettings && settingsItems.length === 0) { - settingsItems.push({ - key: "settings", - label: "Configurable behavior", - detail: settingsQuery.isLoading - ? "Loading setting names…" - : "Setting names are unavailable", - }); - } - const declared = (kind: PluginCapability["kind"]): PluginCapabilityItem[] => plugin.capabilities .filter((capability) => capability.kind === kind) @@ -375,11 +326,6 @@ export function PluginIncludes({ ] : [], }, - { - icon: "Settings", - kind: "Setting", - items: settingsItems, - }, { icon: "Explore", kind: "Skill", diff --git a/apps/app/src/components/tools/PluginDetail.tsx b/apps/app/src/components/tools/PluginDetail.tsx index b8a0cfafc..79c6a8287 100644 --- a/apps/app/src/components/tools/PluginDetail.tsx +++ b/apps/app/src/components/tools/PluginDetail.tsx @@ -1,7 +1,6 @@ import { useState, useSyncExternalStore } from "react"; import { ResourceActivitySection, - ResourceDetailConfigurationSection, ResourceDetailIncludesSection, ResourceDetailOverviewSection, ResourceDetailPage, @@ -22,7 +21,6 @@ import { import { formatHomePathForDisplay } from "@bb/shared-ui/lib/utils"; import { Icon } from "@bb/shared-ui/icon"; import { PluginIcon } from "@/components/plugin/PluginIcon"; -import { PluginSettingsDetail } from "@/components/plugin/PluginSettings"; import { PluginDetailReleaseControl, PluginDetailReleaseStatus, @@ -56,7 +54,6 @@ import { subscribePluginFrontendDiagnostics, type PluginFrontendDiagnostic, } from "@/lib/plugin-frontend"; -import { usePluginSlots } from "@/lib/plugin-slots"; function pluginSourceLabel(plugin: PluginListItem): string | null { return plugin.provenance === "builtin" || plugin.provenance === "catalog" @@ -276,7 +273,6 @@ export function PluginDetail({ onOpenSource: (plugin: PluginListItem) => void; onDelete: (plugin: PluginListItem) => void; }) { - const { settingsSections } = usePluginSlots(); // Hooks run before the loading and not-found returns below, so this has to // tolerate a null plugin rather than read `plugin.id` unconditionally. const sourceQuery = usePluginSource(plugin?.id ?? "", { @@ -304,9 +300,6 @@ export function PluginDetail({ ); } - const hasSettings = - plugin.hasSettings || - settingsSections.some((section) => section.pluginId === plugin.id); const hasUpdateManagement = pluginHasUpdateSurfaces(plugin); const canEditSource = pluginIsLocalSource(plugin); const canRemove = plugin.provenance !== "builtin"; @@ -426,13 +419,8 @@ export function PluginDetail({ - + - {hasSettings ? ( - - - - ) : null} {/* Services and schedules are two different objects with two different status vocabularies, so they stay under their own names and use diff --git a/apps/app/src/components/tools/SkillsCollection.tsx b/apps/app/src/components/tools/SkillsCollection.tsx index af1c6e6a8..0c0245598 100644 --- a/apps/app/src/components/tools/SkillsCollection.tsx +++ b/apps/app/src/components/tools/SkillsCollection.tsx @@ -101,9 +101,15 @@ export function SkillProvenanceTooltip({ {prefix} {name} @@ -160,12 +166,12 @@ function SkillRow({ ) : skill.scope === "plugin" ? ( } accessibleLabel={`${skill.name} is included with ${includedPluginDescription(skill)}`} @@ -238,6 +244,12 @@ export function SkillsOverview({ return RESOURCE_PROVIDER_FILTERS.map((provider) => ({ id: provider, label: providerFilterLabel(provider), + leading: + provider === "bb" ? ( + + ) : ( + + ), disabled: !providerCounts.has(provider) && !providerFilters.includes(provider), })); @@ -534,7 +546,7 @@ export function SkillDetailDialogView({ ), accessibleLabel: `${skill.name} is included with ${includedPluginDescription(skill)}`, diff --git a/apps/app/src/components/tools/detail-page-recipes.test.tsx b/apps/app/src/components/tools/detail-page-recipes.test.tsx index c52706d19..1a82e8224 100644 --- a/apps/app/src/components/tools/detail-page-recipes.test.tsx +++ b/apps/app/src/components/tools/detail-page-recipes.test.tsx @@ -127,7 +127,6 @@ describe("Plugin detail recipe", () => { ["overview", "About"], ["release", "Release"], ["includes", "Capabilities"], - ["configuration", "Settings"], ["activity", "Background services"], ["activity", "Scheduled jobs"], ]); @@ -209,6 +208,38 @@ describe("Plugin detail recipe", () => { } }); + it("collapses long capability descriptions until requested", () => { + const description = "Long capability guidance ".repeat(20).trim(); + const { container } = renderPlugin({ + ...PLUGIN, + capabilities: [ + { + kind: "agent-tool", + id: "long-tool", + label: "Long tool", + detail: description, + }, + ], + }); + + const detail = screen.getByText(description); + expect(detail.className).toContain("line-clamp-3"); + const disclosure = screen.getByRole("button", { + name: "Show full description", + }); + expect(disclosure.getAttribute("aria-expanded")).toBe("false"); + + fireEvent.click(disclosure); + + expect(detail.className).not.toContain("line-clamp-3"); + expect( + screen.getByRole("button", { name: "Show less" }).getAttribute( + "aria-expanded", + ), + ).toBe("true"); + expect(container.textContent).toContain(description); + }); + it("keeps browser-registered app surfaces in Capabilities", () => { setPluginSlotRegistrations("github", { homepageSections: [], diff --git a/apps/app/src/components/tools/plugin-detail-table.tsx b/apps/app/src/components/tools/plugin-detail-table.tsx index 953b624d3..61645a726 100644 --- a/apps/app/src/components/tools/plugin-detail-table.tsx +++ b/apps/app/src/components/tools/plugin-detail-table.tsx @@ -1,3 +1,4 @@ +import { useId, useState } from "react"; import type { ReactNode } from "react"; import { Icon, type IconName } from "@bb/shared-ui/icon"; import { @@ -144,6 +145,9 @@ export function PluginDetailRow({ // A row without detail must not reserve an empty second column, or it hangs a // strip of dead padding off the right edge of the surface. const hasDetail = detail !== null && detail !== undefined && detail !== ""; + const detailId = useId(); + const [expanded, setExpanded] = useState(false); + const isLongDescription = typeof detail === "string" && detail.length > 180; return ( {hasDetail ? ( - {detail} +
+ {detail} +
+ {isLongDescription ? ( + + ) : null} ) : null} diff --git a/apps/app/src/views/SkillsView.test.tsx b/apps/app/src/views/SkillsView.test.tsx index e6c5bcdd3..81efd259a 100644 --- a/apps/app/src/views/SkillsView.test.tsx +++ b/apps/app/src/views/SkillsView.test.tsx @@ -282,7 +282,7 @@ describe("SkillsOverview", () => { ], }); - expect(markup).toContain(">Plugin<"); + expect(markup).toContain(">Included<"); expect(markup).toContain( 'aria-label="automations is included with Automations (bb plugin)"', ); @@ -351,6 +351,11 @@ describe("SkillsOverview", () => { .getByRole("menuitemcheckbox", { name: "Codex" }) .getAttribute("aria-disabled"), ).toBeNull(); + expect( + screen + .getByRole("menuitemcheckbox", { name: "Codex" }) + .querySelector("svg"), + ).not.toBeNull(); expect( screen .getByRole("menuitemcheckbox", { name: "bb" }) @@ -912,7 +917,7 @@ describe("SkillDetailDialogView", () => { manageable: false, }), accessibleLabel: "documents is included with Documents (Codex plugin)", - tooltipName: "Documents plugin", + tooltipName: "Documents plugin.", providerIcon: "codex", }, { @@ -925,7 +930,7 @@ describe("SkillDetailDialogView", () => { }), accessibleLabel: "plugin-notes is included with Skill catalog fixture (bb plugin)", - tooltipName: "Skill catalog fixture plugin", + tooltipName: "Skill catalog fixture plugin.", providerIcon: "bb", }, ])("presents $skill.name as plugin-provided", async (example) => { diff --git a/apps/app/src/views/ToolsView.plugin-detail.test.tsx b/apps/app/src/views/ToolsView.plugin-detail.test.tsx index 2b05913d7..9402290ec 100644 --- a/apps/app/src/views/ToolsView.plugin-detail.test.tsx +++ b/apps/app/src/views/ToolsView.plugin-detail.test.tsx @@ -12,7 +12,6 @@ import { MemoryRouter, Route, Routes } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import { EMPTY_PLUGIN_UPDATE_STATE, - pluginSettingsViewQueryKey, type PluginListItem, } from "@/hooks/queries/plugin-settings-queries"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; @@ -1050,18 +1049,7 @@ describe("PluginDetail capability inventory", () => { }, ], } satisfies PluginListItem; - const { queryClient, wrapper: QueryClientWrapper } = - createQueryClientTestHarness(); - queryClient.setQueryData(pluginSettingsViewQueryKey(plugin.id), { - schema: { - apiToken: { - type: "string", - label: "API token", - secret: true, - }, - }, - values: { apiToken: { set: true } }, - }); + const { wrapper: QueryClientWrapper } = createQueryClientTestHarness(); const { container } = render( @@ -1129,11 +1117,6 @@ describe("PluginDetail capability inventory", () => { "Adds a page to the app sidebar.", "enhance-prompt", "Adds an action beside the thread composer.", - "Advanced preferences", - "Custom settings section", - "API token", - "Setting", - "apiToken", "bb capability", "Inspect contributed capabilities.", "review", @@ -1147,6 +1130,8 @@ describe("PluginDetail capability inventory", () => { ]) { expect(inventory.getByText(text)).toBeTruthy(); } + expect(inventory.queryByText("Advanced preferences")).toBeNull(); + expect(inventory.queryByText("API token")).toBeNull(); expect(inventory.queryByText("watch")).toBeNull(); expect(inventory.queryByText("daily-cleanup")).toBeNull(); expect(includes?.textContent).not.toContain("2 background services"); diff --git a/packages/shared-ui/src/components/ui/resource/toolbar.tsx b/packages/shared-ui/src/components/ui/resource/toolbar.tsx index 9b8173485..d4032098b 100644 --- a/packages/shared-ui/src/components/ui/resource/toolbar.tsx +++ b/packages/shared-ui/src/components/ui/resource/toolbar.tsx @@ -82,19 +82,30 @@ export function ResourceTabDescription({ children }: { children: ReactNode }) { export interface ResourceOption { id: string; label: string; + leading?: ReactNode; description?: string; disabled?: boolean; } function ResourceOptionContent({ option }: { option: ResourceOption }) { return ( - - {option.label} - {option.description ? ( - - {option.description} + + {option.leading ? ( + ) : null} + + {option.label} + {option.description ? ( + + {option.description} + + ) : null} + ); } From ce19ed5fce984b6988aeb225ed147c07033cf946 Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Mon, 3 Aug 2026 19:24:06 -0700 Subject: [PATCH 2/2] Restore categorized official plugin catalog --- .../management/BrowsePluginsTab.test.tsx | 24 ++- .../plugin/management/BrowsePluginsTab.tsx | 154 +++++++++++------- 2 files changed, 112 insertions(+), 66 deletions(-) diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx index 5c10b8868..67b7190e5 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx @@ -1,6 +1,12 @@ // @vitest-environment jsdom -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { + cleanup, + fireEvent, + render, + screen, + within, +} from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { PluginCatalogSearchEntry } from "@/hooks/queries/plugin-catalog-queries"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; @@ -109,6 +115,9 @@ describe("BrowsePluginsTab", () => { ); expect(await screen.findByText("BB Official plugins")).toBeTruthy(); + const officialCatalog = screen.getByRole("region", { + name: "BB Official plugins", + }); const memoryCard = (await screen.findByText("Memory")).closest("div"); expect(memoryCard).not.toBeNull(); // Scoped to the card on purpose: INCOMPATIBLE_ENTRY spreads MEMORY_ENTRY @@ -124,10 +133,17 @@ describe("BrowsePluginsTab", () => { .getByRole("button", { name: "Open GitHub details" }) .closest('[class*="auto-fill"]'); expect(githubGrid?.className).toContain("auto-fill"); - expect(screen.queryByRole("heading", { name: "Productivity" })).toBeNull(); expect( - screen.queryByRole("heading", { name: "Developer tools" }), - ).toBeNull(); + within(officialCatalog).getByRole("heading", { name: "Productivity" }), + ).toBeTruthy(); + expect( + within(officialCatalog).getByRole("heading", { + name: "Developer tools", + }), + ).toBeTruthy(); + expect( + within(officialCatalog).getByRole("button", { name: "Install Memory" }), + ).toBeTruthy(); expect(screen.queryByText(MEMORY_ENTRY.source)).toBeNull(); expect(screen.getByText("Requires a newer BB version")).toBeTruthy(); diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx index 1b535a69e..009ca4392 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx @@ -53,37 +53,23 @@ export function BrowsePluginsTab({ resetKey: debouncedQuery.toLowerCase(), }); + const byCategory = new Map(); + for (const entry of pagination.items) { + const bucket = byCategory.get(entry.category); + if (bucket === undefined) byCategory.set(entry.category, [entry]); + else bucket.push(entry); + } + return ( -
-

- BB Official plugins -

- {status === undefined ? ( -

- {statusQuery.isPending - ? "Loading plugins…" - : "Plugin list unavailable."} -

- ) : ( -

- {status.pluginCount} plugin - {status.pluginCount === 1 ? "" : "s"} · bundled with BB and - installed with one click -

- )} -
- - -
+ } footer={ pagination.total > pagination.pageSize ? ( @@ -98,43 +84,87 @@ export function BrowsePluginsTab({ ) : undefined } > - {searchQuery.isError && entries.length > 0 ? ( -

- Showing cached catalog results because the latest search failed. -

- ) : null} +
+
+

+ BB Official plugins +

+ {status === undefined ? ( +

+ {statusQuery.isPending + ? "Loading plugins…" + : "Plugin list unavailable."} +

+ ) : ( +

+ {status.pluginCount} plugin + {status.pluginCount === 1 ? "" : "s"} · bundled with BB and + installed with one click +

+ )} +
- {searchQuery.isPending ? ( - - ) : entries.length === 0 ? ( - { - void searchQuery.refetch(); - } - : undefined - } - /> - ) : ( - - {pagination.items.map((entry) => ( - - ))} - - )} + {searchQuery.isError && entries.length > 0 ? ( +

+ Showing cached catalog results because the latest search failed. +

+ ) : null} + + {searchQuery.isPending ? ( + + ) : entries.length === 0 ? ( + { + void searchQuery.refetch(); + } + : undefined + } + /> + ) : ( +
+ {[...byCategory.entries()].map(([category, categoryEntries]) => ( +
+

+ {category} +

+ + {categoryEntries.map((entry) => ( + + ))} + +
+ ))} +
+ )} +
); }