From 6e51c290e30b3cd5e3228ff3950ad2e0245d0ebc Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 7 Aug 2026 15:00:53 -0700 Subject: [PATCH] Add the Cloud plugin and AI gateway --- .../AppLayout.plugin-panel-header.test.tsx | 1 + .../AppLayout.root-compose-project.test.tsx | 1 + .../components/plugin/PluginSettings.test.tsx | 19 +- .../plugin/PluginSettingsSections.tsx | 52 +- .../PluginSidebarFooterActions.test.tsx | 38 +- .../plugin/PluginSidebarFooterActions.tsx | 17 +- .../settings/PluginsSettingsSection.test.tsx | 77 +- apps/app/src/lib/route-paths.ts | 10 +- apps/app/src/lib/system-config-atoms.ts | 1 + .../views/SettingsView.experiments.test.tsx | 10 + apps/app/src/views/SettingsView.stories.tsx | 7 + apps/app/src/views/SettingsView.tsx | 24 + .../__tests__/command-output/settings.test.ts | 17 + apps/connect/src/ai-schema.test.ts | 84 ++ apps/connect/src/ai-schema.ts | 49 + apps/connect/src/ai.test.ts | 605 ++++++++++++ apps/connect/src/ai.ts | 490 ++++++++++ apps/connect/src/machine-label.ts | 2 +- apps/connect/src/tunnel-do.ts | 2 + apps/connect/src/worker.test.ts | 2 + apps/connect/src/worker.ts | 7 + apps/connect/wrangler.jsonc | 2 + apps/desktop/test/preload-build.test.ts | 1 + apps/host-daemon/src/codex-chatgpt-client.ts | 2 + .../src/services/ai/cloud-ai-provider.ts | 46 + apps/server/src/services/ai/inference.ts | 78 ++ .../src/services/ai/voice-transcription.ts | 88 +- .../server/src/services/plugins/plugin-api.ts | 6 + .../skills/builtin-skills/bb-cli/SKILL.md | 27 +- .../bb-cli/references/app-settings.md | 11 + .../bb-plugin-authoring/SKILL.md | 31 +- apps/server/test/ai/inference.test.ts | 188 +++- .../test/ai/voice-transcription.test.ts | 177 +++- .../services/plugins/builtin-plugins.test.ts | 13 +- .../plugins/plugin-authoring-docs.test.ts | 1 + apps/server/test/system/experiments.test.ts | 6 + .../threads/thread-runtime-config.test.ts | 1 + docs/api_to_audit.md | 33 +- docs/configuration.md | 42 +- packages/connect-client/src/ai.ts | 152 +++ packages/connect-client/src/index.ts | 6 + .../test/connect-client.test.ts | 107 ++ .../connect-db/migrations/0006_ai_usage.sql | 7 + .../migrations/meta/0006_snapshot.json | 920 ++++++++++++++++++ .../connect-db/migrations/meta/_journal.json | 7 + packages/connect-db/src/constants.ts | 15 + packages/connect-db/src/schema.ts | 21 + packages/db/test/experiments.test.ts | 1 + packages/domain/src/experiments.ts | 1 + packages/plugin-registry/r/icon.json | 2 +- .../bundled-types/bb-plugin-sdk-app.d.ts | 7 +- .../bundled-types/bb-plugin-sdk.d.ts | 84 +- .../src/__tests__/public-types.test.ts | 6 + packages/plugin-sdk/src/app-contract.ts | 5 +- packages/plugin-sdk/src/backend-contract.ts | 58 ++ .../src/testing/fake-plugin-host.ts | 5 + packages/shared-ui/src/components/ui/icon.tsx | 1 + .../src/generated/plugin-sdk-dts.generated.ts | 4 +- .../plugin-starter-files.generated.ts | 2 +- .../src/generated/templates.generated.ts | 2 +- .../src/templates/bb-guide-environments.md | 37 +- plugins/connect/app.test.tsx | 110 ++- plugins/connect/app.tsx | 178 +++- plugins/connect/package.json | 8 +- plugins/connect/src/cli.ts | 67 +- plugins/connect/src/cloud-ai.ts | 148 +++ plugins/connect/src/connect.test.ts | 224 ++++- plugins/connect/src/rpc.ts | 11 + plugins/connect/src/server.ts | 24 +- plugins/connect/src/tunnel.ts | 3 + plugins/connect/src/types.ts | 6 + 71 files changed, 4343 insertions(+), 154 deletions(-) create mode 100644 apps/connect/src/ai-schema.test.ts create mode 100644 apps/connect/src/ai-schema.ts create mode 100644 apps/connect/src/ai.test.ts create mode 100644 apps/connect/src/ai.ts create mode 100644 apps/server/src/services/ai/cloud-ai-provider.ts create mode 100644 packages/connect-client/src/ai.ts create mode 100644 packages/connect-db/migrations/0006_ai_usage.sql create mode 100644 packages/connect-db/migrations/meta/0006_snapshot.json create mode 100644 plugins/connect/src/cloud-ai.ts diff --git a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx index 7dff45cb33..ba54eb2796 100644 --- a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx +++ b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx @@ -28,6 +28,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({ data: { experiments: { claudeCodeMockCliTraffic: false, + cloudAi: false, newOnboarding: false, toolsHub: true, }, diff --git a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx index b2f91fa6c8..98e8cebdf2 100644 --- a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx +++ b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx @@ -24,6 +24,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({ data: { experiments: { claudeCodeMockCliTraffic: false, + cloudAi: false, newOnboarding: false, toolsHub: true, }, diff --git a/apps/app/src/components/plugin/PluginSettings.test.tsx b/apps/app/src/components/plugin/PluginSettings.test.tsx index 68d1ddbdaa..60973357d7 100644 --- a/apps/app/src/components/plugin/PluginSettings.test.tsx +++ b/apps/app/src/components/plugin/PluginSettings.test.tsx @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; import { @@ -283,14 +284,16 @@ describe("PluginSettingsDetail settings gating", () => { }); const { wrapper } = createQueryClientTestHarness(); render( - , + + + , { wrapper }, ); diff --git a/apps/app/src/components/plugin/PluginSettingsSections.tsx b/apps/app/src/components/plugin/PluginSettingsSections.tsx index 92b9101ab0..c2da68444d 100644 --- a/apps/app/src/components/plugin/PluginSettingsSections.tsx +++ b/apps/app/src/components/plugin/PluginSettingsSections.tsx @@ -1,3 +1,6 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; +import { useSystemConfig } from "@/hooks/queries/system-queries"; import { usePluginSlots, type PluginSettingsSectionSlot, @@ -8,6 +11,20 @@ import { ResourceDetailConfigurationSection, } from "@bb/shared-ui/resource-list"; +const CONNECT_PLUGIN_ID = "connect"; +const CLOUD_AI_SECTION_ID = "cloud-ai"; + +function isSettingsSectionVisible( + section: PluginSettingsSectionSlot, + cloudAiEnabled: boolean, +): boolean { + return !( + section.pluginId === CONNECT_PLUGIN_ID && + section.id === CLOUD_AI_SECTION_ID && + !cloudAiEnabled + ); +} + /** * Plugin `settingsSection` slot mounts, rendered on that plugin's canonical * Plugins detail page below the host-rendered declarative form. @@ -15,8 +32,11 @@ import { */ export function PluginSettingsSections({ pluginId }: { pluginId: string }) { const { settingsSections } = usePluginSlots(); + const cloudAiEnabled = useSystemConfig().data?.experiments?.cloudAi === true; const sections = settingsSections.filter( - (section) => section.pluginId === pluginId, + (section) => + section.pluginId === pluginId && + isSettingsSectionVisible(section, cloudAiEnabled), ); if (sections.length === 0) return null; return ; @@ -27,16 +47,34 @@ function PluginSettingsSectionList({ }: { sections: readonly PluginSettingsSectionSlot[]; }) { + const location = useLocation(); + + useEffect(() => { + if (location.hash.length <= 1) return; + let sectionId: string; + try { + sectionId = decodeURIComponent(location.hash.slice(1)); + } catch { + return; + } + if (!sections.some((section) => section.id === sectionId)) return; + document.getElementById(sectionId)?.scrollIntoView({ block: "start" }); + }, [location.hash, location.key, sections]); + return (
{sections.map((section) => { const key = `${section.pluginId}/${section.id}/${section.generation}`; - return section.title === undefined ? ( - - ) : ( - - - + return ( +
+ {section.title === undefined ? ( + + ) : ( + + + + )} +
); })}
diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx index 9ba8114110..a852460af8 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx @@ -32,7 +32,13 @@ function registrationSet( } function LocationProbe() { - return {useLocation().pathname}; + const location = useLocation(); + return ( + + {location.pathname} + {location.hash} + + ); } function renderWithProviders(ui: ReactNode, toolsHubEnabled = false) { @@ -56,7 +62,7 @@ afterEach(() => { }); describe("PluginSidebarFooterActions", () => { - it("prefers branding.icon over the logo and contribution icon", () => { + it("uses the action icon instead of the plugin branding icon", () => { setPluginLogoUrls( new Map([ [ @@ -87,8 +93,8 @@ describe("PluginSidebarFooterActions", () => { renderWithProviders(); - expect(document.querySelector('[data-icon="FileText"]')).not.toBeNull(); - expect(document.querySelector('[data-icon="Smartphone"]')).toBeNull(); + expect(document.querySelector('[data-icon="Smartphone"]')).not.toBeNull(); + expect(document.querySelector('[data-icon="FileText"]')).toBeNull(); expect(document.querySelector("img")).toBeNull(); }); @@ -143,4 +149,28 @@ describe("PluginSidebarFooterActions", () => { ); }, ); + + it("opens a specific plugin settings section", () => { + setPluginSlotRegistrations( + "cloud", + registrationSet({ + sidebarFooterActions: [ + { + id: "remote-access", + title: "Remote access", + icon: "Smartphone", + run: ({ openSettings }) => + openSettings({ sectionId: "remote-access" }), + }, + ], + }), + ); + + renderWithProviders(); + fireEvent.click(screen.getByRole("button", { name: "Remote access" })); + + expect(screen.getByLabelText("Current path").textContent).toBe( + "/settings/plugins/cloud#remote-access", + ); + }); }); diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx index 49e253a954..186b5e1bd6 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx @@ -1,8 +1,9 @@ import { useNavigate } from "react-router-dom"; import { cn } from "@bb/shared-ui/lib/utils"; import { COARSE_POINTER_CHILD_ICON_BUTTON_CLASS } from "@bb/shared-ui/coarse-pointer-sizing"; +import { Icon } from "@bb/shared-ui/icon"; import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar.js"; -import { PluginIcon } from "@/components/plugin/PluginIcon"; +import { pluginIconName } from "@/components/plugin/PluginIcon"; import { usePluginSlots, type PluginSidebarFooterActionSlot, @@ -63,7 +64,11 @@ function PluginSidebarFooterActionList({ }); }} > - +