From e9df4d09a2668bd9d04ba618e650beb5f80da676 Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Thu, 6 Aug 2026 21:57:57 +0000 Subject: [PATCH] Move T3 sidebar plugin to examples --- .../src/services/plugins/builtin-registry.ts | 8 - .../skills/builtin-skills/bb-cli/SKILL.md | 2 +- .../bb-plugin-authoring/SKILL.md | 2 + .../services/plugins/builtin-plugins.test.ts | 4 +- .../services/plugins/official-plugins.test.ts | 1 - docs/configuration.md | 2 +- docs/official-plugin-release-process.md | 13 +- docs/plugin-sidebar-thread-list.md | 3 +- .../plugins}/t3sidebar/README.md | 30 ++- .../plugins}/t3sidebar/app.tsx | 0 .../plugins}/t3sidebar/package.json | 11 +- .../plugins}/t3sidebar/src/Disc.tsx | 0 .../t3sidebar/src/ParentChip.test.tsx | 4 +- .../plugins}/t3sidebar/src/ParentChip.tsx | 4 +- .../plugins}/t3sidebar/src/ProviderGlyph.tsx | 2 +- .../plugins}/t3sidebar/src/RowContextMenu.tsx | 10 +- .../plugins}/t3sidebar/src/SlimRow.tsx | 4 +- .../plugins}/t3sidebar/src/StatusGlyph.tsx | 16 +- .../plugins}/t3sidebar/src/StatusSlot.tsx | 0 .../plugins}/t3sidebar/src/SubagentsChip.tsx | 2 +- .../plugins}/t3sidebar/src/ThreadCard.tsx | 6 +- .../t3sidebar/src/ThreadInbox.test.tsx | 174 +++++++++--------- .../plugins}/t3sidebar/src/ThreadInbox.tsx | 6 +- .../plugins/t3sidebar/src/components/Icon.tsx | 61 ++++++ .../t3sidebar/src/components/Select.tsx | 120 ++++++++++++ .../plugins}/t3sidebar/src/inbox.test.ts | 15 +- .../plugins}/t3sidebar/src/inbox.ts | 4 +- .../plugins/t3sidebar/src/lib/portal-scope.ts | 19 ++ examples/plugins/t3sidebar/src/lib/utils.ts | 6 + .../plugins}/t3sidebar/src/lifecycle.test.ts | 10 +- .../plugins}/t3sidebar/src/lifecycle.ts | 0 .../t3sidebar/src/relative-time.test.ts | 0 .../plugins}/t3sidebar/src/relative-time.ts | 0 .../plugins}/t3sidebar/src/server.ts | 4 +- .../plugins}/t3sidebar/src/useLifecycle.ts | 4 +- .../plugins}/t3sidebar/tsconfig.json | 0 .../plugins}/t3sidebar/vitest.config.ts | 2 +- .../src/generated/templates.generated.ts | 2 +- .../src/templates/bb-guide-plugins.md | 22 +-- pnpm-lock.yaml | 110 ++++++----- 40 files changed, 463 insertions(+), 220 deletions(-) rename {plugins => examples/plugins}/t3sidebar/README.md (68%) rename {plugins => examples/plugins}/t3sidebar/app.tsx (100%) rename {plugins => examples/plugins}/t3sidebar/package.json (80%) rename {plugins => examples/plugins}/t3sidebar/src/Disc.tsx (100%) rename {plugins => examples/plugins}/t3sidebar/src/ParentChip.test.tsx (97%) rename {plugins => examples/plugins}/t3sidebar/src/ParentChip.tsx (94%) rename {plugins => examples/plugins}/t3sidebar/src/ProviderGlyph.tsx (98%) rename {plugins => examples/plugins}/t3sidebar/src/RowContextMenu.tsx (90%) rename {plugins => examples/plugins}/t3sidebar/src/SlimRow.tsx (97%) rename {plugins => examples/plugins}/t3sidebar/src/StatusGlyph.tsx (91%) rename {plugins => examples/plugins}/t3sidebar/src/StatusSlot.tsx (100%) rename {plugins => examples/plugins}/t3sidebar/src/SubagentsChip.tsx (98%) rename {plugins => examples/plugins}/t3sidebar/src/ThreadCard.tsx (98%) rename {plugins => examples/plugins}/t3sidebar/src/ThreadInbox.test.tsx (86%) rename {plugins => examples/plugins}/t3sidebar/src/ThreadInbox.tsx (98%) create mode 100644 examples/plugins/t3sidebar/src/components/Icon.tsx create mode 100644 examples/plugins/t3sidebar/src/components/Select.tsx rename {plugins => examples/plugins}/t3sidebar/src/inbox.test.ts (95%) rename {plugins => examples/plugins}/t3sidebar/src/inbox.ts (97%) create mode 100644 examples/plugins/t3sidebar/src/lib/portal-scope.ts create mode 100644 examples/plugins/t3sidebar/src/lib/utils.ts rename {plugins => examples/plugins}/t3sidebar/src/lifecycle.test.ts (96%) rename {plugins => examples/plugins}/t3sidebar/src/lifecycle.ts (100%) rename {plugins => examples/plugins}/t3sidebar/src/relative-time.test.ts (100%) rename {plugins => examples/plugins}/t3sidebar/src/relative-time.ts (100%) rename {plugins => examples/plugins}/t3sidebar/src/server.ts (99%) rename {plugins => examples/plugins}/t3sidebar/src/useLifecycle.ts (97%) rename {plugins => examples/plugins}/t3sidebar/tsconfig.json (100%) rename {plugins => examples/plugins}/t3sidebar/vitest.config.ts (73%) diff --git a/apps/server/src/services/plugins/builtin-registry.ts b/apps/server/src/services/plugins/builtin-registry.ts index 16b86099a2..8865e4e94d 100644 --- a/apps/server/src/services/plugins/builtin-registry.ts +++ b/apps/server/src/services/plugins/builtin-registry.ts @@ -126,14 +126,6 @@ export const OFFICIAL_PLUGINS = [ defaultEnabled: true, category: "Workflow management", }, - // Replaces the standard sidebar, so users install it deliberately instead - // of receiving a disabled registration by default. - { - name: "t3sidebar", - pluginId: "t3sidebar", - defaultEnabled: false, - category: "Interface", - }, ].map( (plugin): BundledPluginDefinition => ({ ...plugin, diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md index 697e58d126..5141ee36c8 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md @@ -611,7 +611,7 @@ them by mixing ink into canvas), the `--primary` accent, the secondary text tier (except `side-chat`, which is gated by the **"Side chat plugin"** experiment); official plugins install from the bundled store on demand. - **BB Official plugins** (store under `/api/v1/plugin-catalog`): - - BB's official plugins (GitHub, Docs, Memory, Tasks, T3 Sidebar) ship + - BB's official plugins (GitHub, Docs, Memory, and Tasks) ship bundled inside the app and install from the local copy — no network. Installed official plugins are pinned to the bundled copy and update with BB app releases. - `bb plugin search [--json]` — search the official plugins by id, diff --git a/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md index 9333758676..c140917997 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md @@ -1559,6 +1559,8 @@ Remaining reference examples in `examples/plugins/`: `experimental_NewThreadComposer`, plus a thin index backend (kv layout state, background service + realtime), pure row projection, and a bare-letter keymap that coexists with a dozen live composers. +- `t3sidebar` — an inbox-style replacement for the sidebar thread list, with + header chips for child threads and plugin-owned settled and snoozed state. ## Gotchas diff --git a/apps/server/test/services/plugins/builtin-plugins.test.ts b/apps/server/test/services/plugins/builtin-plugins.test.ts index 3853b59ff0..83b901d4eb 100644 --- a/apps/server/test/services/plugins/builtin-plugins.test.ts +++ b/apps/server/test/services/plugins/builtin-plugins.test.ts @@ -187,9 +187,9 @@ describe("builtin plugin reconciliation", () => { it("keeps official plugins bundled but out of the auto-install builtins", () => { const optionalNames = OFFICIAL_PLUGINS.map((plugin) => plugin.name); - for (const name of ["memory", "t3sidebar"]) { + expect(optionalNames).toEqual(["github", "docs", "memory", "tasks"]); + for (const name of optionalNames) { expect(BUILTIN_PLUGINS.map((plugin) => plugin.name)).not.toContain(name); - expect(optionalNames).toContain(name); } expect(OFFICIAL_PLUGINS.every((plugin) => !plugin.autoInstall)).toBe(true); }); diff --git a/apps/server/test/services/plugins/official-plugins.test.ts b/apps/server/test/services/plugins/official-plugins.test.ts index 82ed7e96d0..b2a0335b2d 100644 --- a/apps/server/test/services/plugins/official-plugins.test.ts +++ b/apps/server/test/services/plugins/official-plugins.test.ts @@ -96,7 +96,6 @@ describe("official plugin registry invariants", () => { memory: "Context & knowledge", secrets: "Developer tools", "side-chat": "Agent interaction", - t3sidebar: "Interface", tasks: "Workflow management", workflows: "Workflow management", }; diff --git a/docs/configuration.md b/docs/configuration.md index ebb8466498..26f7e6d463 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -527,7 +527,7 @@ Plugin state lives under the data dir: commands, injected into agent threads) ``` -BB's official plugins (GitHub, Docs, Memory, Tasks, T3 Sidebar) ship bundled +BB's official plugins (GitHub, Docs, Memory, and Tasks) ship bundled inside the app and install from the local bundled copy — no network, no remote catalog. Discover them with `bb plugin search` or Extensions → Plugins → Browse; users cannot add, remove, or configure the official plugin set. Installed official diff --git a/docs/official-plugin-release-process.md b/docs/official-plugin-release-process.md index 653a93c1e4..1555a88404 100644 --- a/docs/official-plugin-release-process.md +++ b/docs/official-plugin-release-process.md @@ -15,13 +15,12 @@ until a user installs them. The official plugins are: -| Directory | Package name | Store entry | Plugin id | -| ---------------------- | ------------------------ | ----------- | -------------- | -| `plugins/github` | `bb-plugin-github` | `github` | `github` | -| `plugins/docs` | `bb-plugin-simple-notes` | `docs` | `simple-notes` | -| `plugins/memory` | `bb-plugin-memory` | `memory` | `memory` | -| `plugins/tasks` | `bb-plugin-tasks` | `tasks` | `tasks` | -| `plugins/t3sidebar` | `bb-plugin-t3sidebar` | `t3sidebar` | `t3sidebar` | +| Directory | Package name | Store entry | Plugin id | +| ---------------- | ------------------------ | ----------- | -------------- | +| `plugins/github` | `bb-plugin-github` | `github` | `github` | +| `plugins/docs` | `bb-plugin-simple-notes` | `docs` | `simple-notes` | +| `plugins/memory` | `bb-plugin-memory` | `memory` | `memory` | +| `plugins/tasks` | `bb-plugin-tasks` | `tasks` | `tasks` | ## Releasing a change diff --git a/docs/plugin-sidebar-thread-list.md b/docs/plugin-sidebar-thread-list.md index 4a05ccfeee..33b2113933 100644 --- a/docs/plugin-sidebar-thread-list.md +++ b/docs/plugin-sidebar-thread-list.md @@ -4,7 +4,8 @@ Status: **implemented**. The members below ship in `@bb/plugin-sdk/app`. This document specifies one exclusive slot and the data surface it needs. A plugin uses them to replace bb's thread list with its own. The reference -consumer is the `t3sidebar` plugin in [`plugins/t3sidebar`](../plugins/t3sidebar). +consumer is the `t3sidebar` plugin in +[`examples/plugins/t3sidebar`](../examples/plugins/t3sidebar). Every member below ships with the `experimental_` prefix and an entry in [api_to_audit.md](api_to_audit.md), per [AGENTS.md](../AGENTS.md). diff --git a/plugins/t3sidebar/README.md b/examples/plugins/t3sidebar/README.md similarity index 68% rename from plugins/t3sidebar/README.md rename to examples/plugins/t3sidebar/README.md index fc11c324dd..7376bbc3a9 100644 --- a/plugins/t3sidebar/README.md +++ b/examples/plugins/t3sidebar/README.md @@ -3,6 +3,13 @@ An inbox-style replacement for bb's sidebar thread list, and the reference example for `app.slots.experimental_threadList`. +This plugin is an example. BB does not bundle it or list it in the official +plugin catalog. Install it from a BB checkout: + +```sh +bb plugin install ./examples/plugins/t3sidebar +``` + Turn it on in **Settings → Appearance → Sidebar**. bb's own list stays the default, and comes back the moment you switch away or disable this plugin. @@ -31,6 +38,7 @@ Three shelves: for a failure, the circle-question for a raised hand, the spinner for live work, and a blue notification dot for a thread that finished while you were not looking. Both lists sit in the same window, so they speak one language. + - **Snoozed** — hidden until a wake time you chose. A snoozed thread comes back early if it starts working or asks you something. - **Settled** — work you are done with, collapsed to one line each. @@ -56,22 +64,24 @@ header shows no parent chip. ## What it demonstrates -| Plugin API | Used for | -| --- | --- | -| `experimental_threadList` | the sidebar's scrolling list (bb keeps the New-thread button, search, nav rows, and footer) | -| `experimental_threadHeaderAction` | the two header chips: children on a parent, and the way back on a child | -| `experimental_useSidebarThreads` | live threads and projects, from the host's own cache | -| `experimental_useSidebarThreadActions` | open, open-in-split, new thread | -| `experimental_useSidebarThreadSplit` | dragging a card out to a split pane | -| `experimental_useSidebarThreadPullRequest` | the `#412` badge, coloured by bb's attention state | -| `@radix-ui/react-context-menu` (shimmed) | this plugin's own right-click menu, built on the action hook | -| `bb.storage.database()` + `bb.rpc` + `bb.realtime` | the settled/snoozed store | +| Plugin API | Used for | +| -------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `experimental_threadList` | the sidebar's scrolling list (bb keeps the New-thread button, search, nav rows, and footer) | +| `experimental_threadHeaderAction` | the two header chips: children on a parent, and the way back on a child | +| `experimental_useSidebarThreads` | live threads and projects, from the host's own cache | +| `experimental_useSidebarThreadActions` | open, open-in-split, new thread | +| `experimental_useSidebarThreadSplit` | dragging a card out to a split pane | +| `experimental_useSidebarThreadPullRequest` | the `#412` badge, coloured by bb's attention state | +| `@radix-ui/react-context-menu` (shimmed) | this plugin's own right-click menu, built on the action hook | +| `bb.storage.database()` + `bb.rpc` + `bb.realtime` | the settled/snoozed store | The plugin API ships **no components**. Status glyphs and the right-click menu are both this plugin's own: `indicator` arrives as data, and every menu item is one call on `experimental_useSidebarThreadActions`. Choosing them is the point of a replaced sidebar. Deletion still routes through `requestDelete`, so BB shows its confirmation dialog rather than a plugin deleting a subtree silently. +The small icon and select components also live in this example. The example +does not import BB's private shared UI package. ## Where the lifecycle lives diff --git a/plugins/t3sidebar/app.tsx b/examples/plugins/t3sidebar/app.tsx similarity index 100% rename from plugins/t3sidebar/app.tsx rename to examples/plugins/t3sidebar/app.tsx diff --git a/plugins/t3sidebar/package.json b/examples/plugins/t3sidebar/package.json similarity index 80% rename from plugins/t3sidebar/package.json rename to examples/plugins/t3sidebar/package.json index d19e8da670..1aafd24527 100644 --- a/plugins/t3sidebar/package.json +++ b/examples/plugins/t3sidebar/package.json @@ -5,7 +5,8 @@ "type": "module", "description": "An inbox-style sidebar: one flat list of cards that never re-orders.", "engines": { - "bb": ">=0.0" + "bb": ">=0.0", + "bbPluginSdk": "^0.4.1" }, "bb": { "name": "t3sidebar", @@ -24,7 +25,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bb/shared-ui": "workspace:*", + "@hugeicons/core-free-icons": "^4.1.3", + "@hugeicons/react": "^1.1.6", + "clsx": "^2.1.1", + "tailwind-merge": "^3.4.0", "zod": "^4.3.6" }, "devDependencies": { @@ -40,6 +44,7 @@ "vitest": "^4.1.1", "@types/better-sqlite3": "^7.6.12", "better-sqlite3": "12.10.0", - "@radix-ui/react-context-menu": "^2.2.16" + "@radix-ui/react-context-menu": "^2.3.3", + "@radix-ui/react-select": "^2.3.3" } } diff --git a/plugins/t3sidebar/src/Disc.tsx b/examples/plugins/t3sidebar/src/Disc.tsx similarity index 100% rename from plugins/t3sidebar/src/Disc.tsx rename to examples/plugins/t3sidebar/src/Disc.tsx diff --git a/plugins/t3sidebar/src/ParentChip.test.tsx b/examples/plugins/t3sidebar/src/ParentChip.test.tsx similarity index 97% rename from plugins/t3sidebar/src/ParentChip.test.tsx rename to examples/plugins/t3sidebar/src/ParentChip.test.tsx index 8cedb1832a..0528ac93ea 100644 --- a/plugins/t3sidebar/src/ParentChip.test.tsx +++ b/examples/plugins/t3sidebar/src/ParentChip.test.tsx @@ -7,7 +7,9 @@ import type { PluginSidebarThread } from "@bb/plugin-sdk"; // Load through the harness so the plugin's `@bb/plugin-sdk/app` import binds // to the test runtime. const app = await loadPluginApp(() => import("../app")); -const parentChip = app.threadHeaderActions.find((slot) => slot.id === "parent")!; +const parentChip = app.threadHeaderActions.find( + (slot) => slot.id === "parent", +)!; function thread( overrides: Partial = {}, diff --git a/plugins/t3sidebar/src/ParentChip.tsx b/examples/plugins/t3sidebar/src/ParentChip.tsx similarity index 94% rename from plugins/t3sidebar/src/ParentChip.tsx rename to examples/plugins/t3sidebar/src/ParentChip.tsx index 0072fb563f..61be9be231 100644 --- a/plugins/t3sidebar/src/ParentChip.tsx +++ b/examples/plugins/t3sidebar/src/ParentChip.tsx @@ -3,8 +3,8 @@ import { experimental_useSidebarThreads as useSidebarThreads, type PluginThreadHeaderActionProps, } from "@bb/plugin-sdk/app"; -import { Icon } from "@bb/shared-ui/icon"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { Icon } from "./components/Icon"; +import { cn } from "./lib/utils"; import { Disc } from "./Disc"; import { parentOf, threadDisplayTitle } from "./inbox"; diff --git a/plugins/t3sidebar/src/ProviderGlyph.tsx b/examples/plugins/t3sidebar/src/ProviderGlyph.tsx similarity index 98% rename from plugins/t3sidebar/src/ProviderGlyph.tsx rename to examples/plugins/t3sidebar/src/ProviderGlyph.tsx index fb1bd5cf63..f6364606f5 100644 --- a/plugins/t3sidebar/src/ProviderGlyph.tsx +++ b/examples/plugins/t3sidebar/src/ProviderGlyph.tsx @@ -1,4 +1,4 @@ -import { cn } from "@bb/shared-ui/lib/utils"; +import { cn } from "./lib/utils"; import { TRAILING_GLYPH_BOX_CLASS } from "./StatusSlot"; /** diff --git a/plugins/t3sidebar/src/RowContextMenu.tsx b/examples/plugins/t3sidebar/src/RowContextMenu.tsx similarity index 90% rename from plugins/t3sidebar/src/RowContextMenu.tsx rename to examples/plugins/t3sidebar/src/RowContextMenu.tsx index d419dcdfae..c028cc1567 100644 --- a/plugins/t3sidebar/src/RowContextMenu.tsx +++ b/examples/plugins/t3sidebar/src/RowContextMenu.tsx @@ -4,7 +4,7 @@ import { experimental_useSidebarThreadActions as useSidebarThreadActions, type PluginSidebarThread, } from "@bb/plugin-sdk/app"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { cn } from "./lib/utils"; /** * This sidebar's own right-click menu. @@ -36,10 +36,14 @@ export function RowContextMenu({ Open in split - void actions.setRead(thread.id, thread.isUnread)}> + void actions.setRead(thread.id, thread.isUnread)} + > {thread.isUnread ? "Mark read" : "Mark unread"} - void actions.setPinned(thread.id, !thread.isPinned)}> + void actions.setPinned(thread.id, !thread.isPinned)} + > {thread.isPinned ? "Unpin" : "Pin"} diff --git a/plugins/t3sidebar/src/SlimRow.tsx b/examples/plugins/t3sidebar/src/SlimRow.tsx similarity index 97% rename from plugins/t3sidebar/src/SlimRow.tsx rename to examples/plugins/t3sidebar/src/SlimRow.tsx index e09e2d1e0b..c4af91f90b 100644 --- a/plugins/t3sidebar/src/SlimRow.tsx +++ b/examples/plugins/t3sidebar/src/SlimRow.tsx @@ -2,8 +2,8 @@ import { experimental_useSidebarThreadActions as useSidebarThreadActions, type PluginSidebarThread, } from "@bb/plugin-sdk/app"; -import { Icon } from "@bb/shared-ui/icon"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { Icon } from "./components/Icon"; +import { cn } from "./lib/utils"; import { RowContextMenu } from "./RowContextMenu"; import { STATUS_SLOT_CLASS, StatusOrTime } from "./StatusSlot"; import { threadDisplayTitle } from "./inbox"; diff --git a/plugins/t3sidebar/src/StatusGlyph.tsx b/examples/plugins/t3sidebar/src/StatusGlyph.tsx similarity index 91% rename from plugins/t3sidebar/src/StatusGlyph.tsx rename to examples/plugins/t3sidebar/src/StatusGlyph.tsx index 43cdc8d515..f8d2925899 100644 --- a/plugins/t3sidebar/src/StatusGlyph.tsx +++ b/examples/plugins/t3sidebar/src/StatusGlyph.tsx @@ -1,6 +1,6 @@ import type { PluginSidebarThreadIndicator } from "@bb/plugin-sdk"; -import { Icon } from "@bb/shared-ui/icon"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { Icon } from "./components/Icon"; +import { cn } from "./lib/utils"; /** * This plugin's status glyphs, matching bb's own sidebar shape for shape: the @@ -60,7 +60,11 @@ export function StatusGlyph({ switch (indicator) { case "unread-error": return ( - + ); case "waiting-for-input": return ( @@ -91,7 +95,11 @@ export function StatusGlyph({ case "draft": case "working-draft": return ( - + ); case "unread-success": // The notification dot, in a box the size of every other glyph, the way diff --git a/plugins/t3sidebar/src/StatusSlot.tsx b/examples/plugins/t3sidebar/src/StatusSlot.tsx similarity index 100% rename from plugins/t3sidebar/src/StatusSlot.tsx rename to examples/plugins/t3sidebar/src/StatusSlot.tsx diff --git a/plugins/t3sidebar/src/SubagentsChip.tsx b/examples/plugins/t3sidebar/src/SubagentsChip.tsx similarity index 98% rename from plugins/t3sidebar/src/SubagentsChip.tsx rename to examples/plugins/t3sidebar/src/SubagentsChip.tsx index ba8f85570c..ef723d4648 100644 --- a/plugins/t3sidebar/src/SubagentsChip.tsx +++ b/examples/plugins/t3sidebar/src/SubagentsChip.tsx @@ -5,7 +5,7 @@ import { type PluginSidebarThread, type PluginThreadHeaderActionProps, } from "@bb/plugin-sdk/app"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { cn } from "./lib/utils"; import { Disc } from "./Disc"; import { StatusGlyph } from "./StatusGlyph"; import { childrenOf, threadDisplayTitle } from "./inbox"; diff --git a/plugins/t3sidebar/src/ThreadCard.tsx b/examples/plugins/t3sidebar/src/ThreadCard.tsx similarity index 98% rename from plugins/t3sidebar/src/ThreadCard.tsx rename to examples/plugins/t3sidebar/src/ThreadCard.tsx index 28cbedf482..bb9471b939 100644 --- a/plugins/t3sidebar/src/ThreadCard.tsx +++ b/examples/plugins/t3sidebar/src/ThreadCard.tsx @@ -4,8 +4,8 @@ import { experimental_useSidebarThreadActions as useSidebarThreadActions, type PluginSidebarThread, } from "@bb/plugin-sdk/app"; -import { Icon } from "@bb/shared-ui/icon"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { Icon, type IconName } from "./components/Icon"; +import { cn } from "./lib/utils"; import { RowContextMenu } from "./RowContextMenu"; import { ProviderGlyph } from "./ProviderGlyph"; import { STATUS_SLOT_CLASS, StatusOrTime } from "./StatusSlot"; @@ -182,7 +182,7 @@ function ParkButton({ onActivate, }: { label: string; - icon: "Clock" | "Check"; + icon: Extract; onActivate: () => void; }) { return ( diff --git a/plugins/t3sidebar/src/ThreadInbox.test.tsx b/examples/plugins/t3sidebar/src/ThreadInbox.test.tsx similarity index 86% rename from plugins/t3sidebar/src/ThreadInbox.test.tsx rename to examples/plugins/t3sidebar/src/ThreadInbox.test.tsx index 906e581ecd..929b7338c4 100644 --- a/plugins/t3sidebar/src/ThreadInbox.test.tsx +++ b/examples/plugins/t3sidebar/src/ThreadInbox.test.tsx @@ -1,6 +1,12 @@ // @vitest-environment jsdom import { afterEach, describe, expect, it } from "vitest"; -import { cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react"; +import { + cleanup, + fireEvent, + screen, + waitFor, + within, +} from "@testing-library/react"; import { loadPluginApp, renderSlot } from "@bb/plugin-sdk/testing/app"; import type { PluginSidebarThread } from "@bb/plugin-sdk"; @@ -10,7 +16,9 @@ import type { PluginSidebarThread } from "@bb/plugin-sdk"; const app = await loadPluginApp(() => import("../app")); const inbox = app.threadLists[0]!; -function thread(overrides: Partial = {}): PluginSidebarThread { +function thread( + overrides: Partial = {}, +): PluginSidebarThread { return { id: "thr_1", projectId: "proj_1", @@ -52,9 +60,10 @@ const listProps = { searchQuery: "", }; -function render(threads: PluginSidebarThread[], projects = [ - { id: "proj_1", name: "bb", isPersonal: false }, -]) { +function render( + threads: PluginSidebarThread[], + projects = [{ id: "proj_1", name: "bb", isPersonal: false }], +) { return renderSlot(inbox, listProps, { sidebarThreads: { status: "ready", threads, projects }, // The lifecycle store is the plugin's own backend; an empty one means @@ -188,7 +197,6 @@ describe("ThreadInbox", () => { expect(screen.getByText("In other")).toBeDefined(); }); - it("hides archived threads", () => { render([thread({ id: "a", isArchived: true })]); expect(screen.queryAllByRole("listitem")).toHaveLength(0); @@ -202,29 +210,25 @@ describe("ThreadInbox", () => { describe("parking threads", () => { it("moves a settled thread to the Settled shelf", async () => { - renderSlot( - inbox, - listProps, - { - sidebarThreads: { - status: "ready", - threads: [thread({ id: "thr_done", title: "Finished work" })], - projects: [{ id: "proj_1", name: "bb", isPersonal: false }], - }, - rpc: { - listLifecycle: () => ({ - rows: [ - { - threadId: "thr_done", - settledAt: 200, - snoozedUntil: null, - snoozedAt: null, - }, - ], - }), - }, + renderSlot(inbox, listProps, { + sidebarThreads: { + status: "ready", + threads: [thread({ id: "thr_done", title: "Finished work" })], + projects: [{ id: "proj_1", name: "bb", isPersonal: false }], }, - ); + rpc: { + listLifecycle: () => ({ + rows: [ + { + threadId: "thr_done", + settledAt: 200, + snoozedUntil: null, + snoozedAt: null, + }, + ], + }), + }, + }); // The shelf renders once the lifecycle read resolves. const shelf = await screen.findByRole("region", { name: "Settled" }); expect(within(shelf).getByText(/Settled \(1\)/)).toBeDefined(); @@ -235,43 +239,39 @@ describe("parking threads", () => { }); it("keeps a working thread out of the shelves and offers no park action", async () => { - renderSlot( - inbox, - listProps, - { - sidebarThreads: { - status: "ready", - threads: [ - thread({ - id: "thr_busy", - title: "Still running", - indicator: "runtime", - activity: { - workflows: 0, - backgroundAgents: 0, - backgroundCommands: 0, - planMode: 0, - goals: 0, - }, - }), - ], - projects: [{ id: "proj_1", name: "bb", isPersonal: false }], - }, - // Settled in the store, but still working: it must stay visible. - rpc: { - listLifecycle: () => ({ - rows: [ - { - threadId: "thr_busy", - settledAt: 200, - snoozedUntil: null, - snoozedAt: null, - }, - ], + renderSlot(inbox, listProps, { + sidebarThreads: { + status: "ready", + threads: [ + thread({ + id: "thr_busy", + title: "Still running", + indicator: "runtime", + activity: { + workflows: 0, + backgroundAgents: 0, + backgroundCommands: 0, + planMode: 0, + goals: 0, + }, }), - }, + ], + projects: [{ id: "proj_1", name: "bb", isPersonal: false }], }, - ); + // Settled in the store, but still working: it must stay visible. + rpc: { + listLifecycle: () => ({ + rows: [ + { + threadId: "thr_busy", + settledAt: 200, + snoozedUntil: null, + snoozedAt: null, + }, + ], + }), + }, + }); expect(await screen.findByText("Still running")).toBeDefined(); expect(screen.queryByRole("region", { name: "Settled" })).toBeNull(); expect(screen.queryByLabelText("Settle thread")).toBeNull(); @@ -307,29 +307,25 @@ describe("parking threads", () => { it("shows the wake countdown on a snoozed row", async () => { const wakeAt = Date.now() + 2 * 60 * 60 * 1000; - renderSlot( - inbox, - listProps, - { - sidebarThreads: { - status: "ready", - threads: [thread({ id: "thr_snz", title: "Later" })], - projects: [{ id: "proj_1", name: "bb", isPersonal: false }], - }, - rpc: { - listLifecycle: () => ({ - rows: [ - { - threadId: "thr_snz", - settledAt: null, - snoozedUntil: wakeAt, - snoozedAt: Date.now(), - }, - ], - }), - }, + renderSlot(inbox, listProps, { + sidebarThreads: { + status: "ready", + threads: [thread({ id: "thr_snz", title: "Later" })], + projects: [{ id: "proj_1", name: "bb", isPersonal: false }], }, - ); + rpc: { + listLifecycle: () => ({ + rows: [ + { + threadId: "thr_snz", + settledAt: null, + snoozedUntil: wakeAt, + snoozedAt: Date.now(), + }, + ], + }), + }, + }); const shelf = await screen.findByRole("region", { name: "Snoozed" }); fireEvent.click(within(shelf).getByRole("button")); expect(within(shelf).getByText("2h")).toBeDefined(); @@ -349,13 +345,7 @@ describe("row context menu", () => { within(menu) .getAllByRole("menuitem") .map((item) => item.textContent), - ).toEqual([ - "Open in split", - "Mark unread", - "Pin", - "Archive", - "Delete", - ]); + ).toEqual(["Open in split", "Mark unread", "Pin", "Archive", "Delete"]); }); it("routes deletion through the host's confirmation", async () => { diff --git a/plugins/t3sidebar/src/ThreadInbox.tsx b/examples/plugins/t3sidebar/src/ThreadInbox.tsx similarity index 98% rename from plugins/t3sidebar/src/ThreadInbox.tsx rename to examples/plugins/t3sidebar/src/ThreadInbox.tsx index 0a10fdb3f5..021bb36714 100644 --- a/plugins/t3sidebar/src/ThreadInbox.tsx +++ b/examples/plugins/t3sidebar/src/ThreadInbox.tsx @@ -5,15 +5,15 @@ import { type PluginSidebarThread, type PluginThreadListProps, } from "@bb/plugin-sdk/app"; -import { Icon } from "@bb/shared-ui/icon"; -import { cn } from "@bb/shared-ui/lib/utils"; +import { Icon } from "./components/Icon"; +import { cn } from "./lib/utils"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@bb/shared-ui/select"; +} from "./components/Select"; import { ThreadCard } from "./ThreadCard"; import { SlimRow } from "./SlimRow"; import { useLifecycle } from "./useLifecycle"; diff --git a/examples/plugins/t3sidebar/src/components/Icon.tsx b/examples/plugins/t3sidebar/src/components/Icon.tsx new file mode 100644 index 0000000000..b6ef720109 --- /dev/null +++ b/examples/plugins/t3sidebar/src/components/Icon.tsx @@ -0,0 +1,61 @@ +import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; +import { + ArrowDown01Icon, + ArrowLeft01Icon, + ArrowTurnBackwardIcon, + ArrowUp01Icon, + CancelCircleIcon, + CheckListIcon, + Clock01Icon, + ComputerTerminal01Icon, + Edit02Icon, + HelpCircleIcon, + Loading03Icon, + Target02Icon, + Tick02Icon, + UserAdd01Icon, + WorkflowCircle03Icon, +} from "@hugeicons/core-free-icons"; +import { cn } from "../lib/utils"; + +const ICON_MAP = { + ArrowTurnBackward: ArrowTurnBackwardIcon, + Check: Tick02Icon, + ChevronDown: ArrowDown01Icon, + ChevronLeft: ArrowLeft01Icon, + ChevronUp: ArrowUp01Icon, + CircleQuestion: HelpCircleIcon, + CircleX: CancelCircleIcon, + Clock: Clock01Icon, + Edit: Edit02Icon, + ListTodo: CheckListIcon, + Loading: Loading03Icon, + Target: Target02Icon, + Terminal: ComputerTerminal01Icon, + UserRoundPlus: UserAdd01Icon, + Workflow: WorkflowCircle03Icon, +} as const satisfies Record; + +export type IconName = keyof typeof ICON_MAP; + +export function Icon({ + name, + className, + "aria-hidden": ariaHidden, + "aria-label": ariaLabel, +}: { + name: IconName; + className?: string; + "aria-hidden"?: boolean | "true" | "false"; + "aria-label"?: string; +}) { + return ( + + ); +} diff --git a/examples/plugins/t3sidebar/src/components/Select.tsx b/examples/plugins/t3sidebar/src/components/Select.tsx new file mode 100644 index 0000000000..cc1d760a72 --- /dev/null +++ b/examples/plugins/t3sidebar/src/components/Select.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Icon } from "./Icon"; +import { cn } from "../lib/utils"; +import { usePortalScopeProps } from "../lib/portal-scope"; + +const Select = SelectPrimitive.Root; +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue }; diff --git a/plugins/t3sidebar/src/inbox.test.ts b/examples/plugins/t3sidebar/src/inbox.test.ts similarity index 95% rename from plugins/t3sidebar/src/inbox.test.ts rename to examples/plugins/t3sidebar/src/inbox.test.ts index e27b185a66..3581891b4c 100644 --- a/plugins/t3sidebar/src/inbox.test.ts +++ b/examples/plugins/t3sidebar/src/inbox.test.ts @@ -12,7 +12,9 @@ import { visibleInboxThreads, } from "./inbox"; -function thread(overrides: Partial = {}): PluginSidebarThread { +function thread( + overrides: Partial = {}, +): PluginSidebarThread { return { id: "thr_1", projectId: "proj_1", @@ -78,7 +80,10 @@ describe("sortByCreatedAtDescending", () => { }); it("does not mutate its input", () => { - const input = [thread({ id: "a", createdAt: 1 }), thread({ id: "b", createdAt: 2 })]; + const input = [ + thread({ id: "a", createdAt: 1 }), + thread({ id: "b", createdAt: 2 }), + ]; sortByCreatedAtDescending(input); expect(input.map((t) => t.id)).toEqual(["a", "b"]); }); @@ -90,9 +95,9 @@ describe("threadDisplayTitle", () => { expect( threadDisplayTitle(thread({ title: null, titleFallback: "Fallback" })), ).toBe("Fallback"); - expect(threadDisplayTitle(thread({ title: null, titleFallback: null }))).toBe( - "Untitled thread", - ); + expect( + threadDisplayTitle(thread({ title: null, titleFallback: null })), + ).toBe("Untitled thread"); }); it("treats a whitespace-only title as absent", () => { diff --git a/plugins/t3sidebar/src/inbox.ts b/examples/plugins/t3sidebar/src/inbox.ts similarity index 97% rename from plugins/t3sidebar/src/inbox.ts rename to examples/plugins/t3sidebar/src/inbox.ts index fa71ec4547..6308f7be9c 100644 --- a/plugins/t3sidebar/src/inbox.ts +++ b/examples/plugins/t3sidebar/src/inbox.ts @@ -12,7 +12,8 @@ export function sortByCreatedAtDescending< T extends { readonly id: string; readonly createdAt: number }, >(threads: readonly T[]): T[] { return [...threads].sort( - (left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id), + (left, right) => + right.createdAt - left.createdAt || left.id.localeCompare(right.id), ); } @@ -35,7 +36,6 @@ export function searchThreadsByTitle( ); } - export interface ProjectScope { /** Project id, or null for "all projects". */ id: string | null; diff --git a/examples/plugins/t3sidebar/src/lib/portal-scope.ts b/examples/plugins/t3sidebar/src/lib/portal-scope.ts new file mode 100644 index 0000000000..b6d92c690c --- /dev/null +++ b/examples/plugins/t3sidebar/src/lib/portal-scope.ts @@ -0,0 +1,19 @@ +/** + * Portaled content leaves the plugin mount. These attributes restore the + * plugin style scope and mark the content as an interactive overlay. + */ +declare const __BB_PLUGIN_ID__: string | undefined; + +export function usePortalScopeProps(): { + "data-bb-portaled-overlay": ""; + "data-bb-plugin-root": ""; + "data-bb-plugin"?: string; +} { + const pluginId = + typeof __BB_PLUGIN_ID__ === "string" ? __BB_PLUGIN_ID__ : undefined; + return { + "data-bb-portaled-overlay": "", + "data-bb-plugin-root": "", + ...(pluginId === undefined ? {} : { "data-bb-plugin": pluginId }), + }; +} diff --git a/examples/plugins/t3sidebar/src/lib/utils.ts b/examples/plugins/t3sidebar/src/lib/utils.ts new file mode 100644 index 0000000000..365058cebd --- /dev/null +++ b/examples/plugins/t3sidebar/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/plugins/t3sidebar/src/lifecycle.test.ts b/examples/plugins/t3sidebar/src/lifecycle.test.ts similarity index 96% rename from plugins/t3sidebar/src/lifecycle.test.ts rename to examples/plugins/t3sidebar/src/lifecycle.test.ts index 5119d0cd7a..5f72355e76 100644 --- a/plugins/t3sidebar/src/lifecycle.test.ts +++ b/examples/plugins/t3sidebar/src/lifecycle.test.ts @@ -17,7 +17,9 @@ const quiet: ThreadActivitySignals = { latestAttentionAt: 0, }; -const row = (overrides: Partial = {}): ThreadLifecycleRow => ({ +const row = ( + overrides: Partial = {}, +): ThreadLifecycleRow => ({ threadId: "thr_1", settledAt: null, snoozedUntil: null, @@ -52,7 +54,11 @@ describe("resolveShelf", () => { it("brings a settled thread back when it starts working", () => { expect( - resolveShelf(row({ settledAt: 500 }), { ...quiet, isWorking: true }, 1_000), + resolveShelf( + row({ settledAt: 500 }), + { ...quiet, isWorking: true }, + 1_000, + ), ).toBe("active"); }); diff --git a/plugins/t3sidebar/src/lifecycle.ts b/examples/plugins/t3sidebar/src/lifecycle.ts similarity index 100% rename from plugins/t3sidebar/src/lifecycle.ts rename to examples/plugins/t3sidebar/src/lifecycle.ts diff --git a/plugins/t3sidebar/src/relative-time.test.ts b/examples/plugins/t3sidebar/src/relative-time.test.ts similarity index 100% rename from plugins/t3sidebar/src/relative-time.test.ts rename to examples/plugins/t3sidebar/src/relative-time.test.ts diff --git a/plugins/t3sidebar/src/relative-time.ts b/examples/plugins/t3sidebar/src/relative-time.ts similarity index 100% rename from plugins/t3sidebar/src/relative-time.ts rename to examples/plugins/t3sidebar/src/relative-time.ts diff --git a/plugins/t3sidebar/src/server.ts b/examples/plugins/t3sidebar/src/server.ts similarity index 99% rename from plugins/t3sidebar/src/server.ts rename to examples/plugins/t3sidebar/src/server.ts index b797c65735..7c46a2d110 100644 --- a/plugins/t3sidebar/src/server.ts +++ b/examples/plugins/t3sidebar/src/server.ts @@ -95,7 +95,9 @@ export default function plugin(bb: BbPluginApi) { }; const clear = (threadId: string): void => { - db.prepare(`DELETE FROM thread_lifecycle WHERE thread_id = ?`).run(threadId); + db.prepare(`DELETE FROM thread_lifecycle WHERE thread_id = ?`).run( + threadId, + ); bb.realtime.publish(LIFECYCLE_CHANNEL, { threadId }); }; diff --git a/plugins/t3sidebar/src/useLifecycle.ts b/examples/plugins/t3sidebar/src/useLifecycle.ts similarity index 97% rename from plugins/t3sidebar/src/useLifecycle.ts rename to examples/plugins/t3sidebar/src/useLifecycle.ts index e23d181340..662103caae 100644 --- a/plugins/t3sidebar/src/useLifecycle.ts +++ b/examples/plugins/t3sidebar/src/useLifecycle.ts @@ -41,7 +41,9 @@ export interface LifecycleApi { * move its row without waiting for an unrelated re-render, and re-reading the * clock during render would make the classification unstable. */ -export function useLifecycle(threads: readonly PluginSidebarThread[]): LifecycleApi { +export function useLifecycle( + threads: readonly PluginSidebarThread[], +): LifecycleApi { const rpc = useRpc(); const [rows, setRows] = useState>( () => new Map(), diff --git a/plugins/t3sidebar/tsconfig.json b/examples/plugins/t3sidebar/tsconfig.json similarity index 100% rename from plugins/t3sidebar/tsconfig.json rename to examples/plugins/t3sidebar/tsconfig.json diff --git a/plugins/t3sidebar/vitest.config.ts b/examples/plugins/t3sidebar/vitest.config.ts similarity index 73% rename from plugins/t3sidebar/vitest.config.ts rename to examples/plugins/t3sidebar/vitest.config.ts index 0ba2f30d98..7ad271f2b6 100644 --- a/plugins/t3sidebar/vitest.config.ts +++ b/examples/plugins/t3sidebar/vitest.config.ts @@ -1,4 +1,4 @@ -import { defineWorkspaceTestConfig } from "../../vitest.shared.js"; +import { defineWorkspaceTestConfig } from "../../../vitest.shared.js"; export default defineWorkspaceTestConfig({ test: { diff --git a/packages/templates/src/generated/templates.generated.ts b/packages/templates/src/generated/templates.generated.ts index aa7b049d9b..5b76c30986 100644 --- a/packages/templates/src/generated/templates.generated.ts +++ b/packages/templates/src/generated/templates.generated.ts @@ -83,7 +83,7 @@ export const templateDefinitions = [ }, { "id": "bbGuidePlugins", - "body": "Plugin commands\n\nA bb plugin is a TypeScript package that extends the bb server in-process:\nbackground services, cron schedules, HTTP/RPC endpoints, thread lifecycle\nhandlers, settings, storage — and `bb` CLI subcommands that agents and humans\nrun like any other command. Plugins are full-trust code inside the server.\n\nPlugins are on by default. Builtin plugins (`builtin:`) ship with bb;\nuser-installed plugins come from `bb plugin install` or the official store.\nPlugin state lives under `/plugins//` (per-plugin SQLite file,\nsecrets, logs).\n\nThe builtin Custom instructions plugin adds a multiline editor under Settings\n→ Custom instructions. Saved text is persisted on this bb host and included in\nagent task instructions; blank text contributes nothing.\n\nThe builtin Workflows plugin runs durable provider-independent JavaScript\norchestration. It is disabled on fresh installations; enable `workflows` under\nExtensions → Plugins or run `bb plugin enable workflows` before using:\n\n bb workflows validate (--script ''|--source ''|\n --file |--name )\n bb workflows run (--script ''|--source ''|\n --file |--name )\n [--args ''] [--resume ]\n bb workflows status \n bb workflows history [--cursor ] [--limit <1-100>]\n bb workflows list [--limit <1-50>]\n bb workflows stop \n\nCommands must run from a BB project thread. Workflows has six plugin\nsettings, configurable with `bb plugin config workflows set `:\n`maxActiveRuns` (default 4, range 1–32), `maxConcurrentAgents` (8, 1–64),\n`maxAgentCalls` (100, 1–1000), `totalRunTimeoutMs` (86400000, 60000–604800000),\n`retentionDays` (30, 1–3650), and `maxNotificationBytes` (16384,\n1024–262144). `maxActiveRuns` applies live; the other five are snapshotted for\neach new run. Settings changes do not require a plugin reload.\n\n`status` is a bounded polling summary, and `list` returns only compact run\nsummaries. Detailed run and call records are paged JSONL: redirect `history`\ninto `$BB_THREAD_STORAGE` before inspecting it, and continue with the final\npage record's `nextCursor`. The invoking shell writes\nthat file on the thread's execution host, so this works the same on local and\nremote hosts without granting the plugin arbitrary filesystem access. Use `bb\nprovider list --environment \"$BB_ENVIRONMENT_ID\" --json` and then `bb provider\nmodels --environment \"$BB_ENVIRONMENT_ID\" --json` before writing\nan explicit selection; never guess ACP model IDs.\n\nThe Memory plugin is an opt-in install, bundled with the app:\n`bb plugin install memory`. Once installed, it injects a compact global and\ncurrent-project memory index into agent context and progressively discloses\nfull records through CLI-only commands. Because its store works across\nproviders, we recommend disabling provider-native memory under Settings →\nProviders to avoid duplicate or conflicting stores. Settings → Memory lists\nevery global and project memory and supports version-checked edits and soft\ndeletion.\n\n bb memory catalog [--scope project|global|all] [--json]\n bb memory search [--scope project|global|all] [--json]\n bb memory get [--scope project|global|all] [--json]\n bb memory add --scope project|global --name --summary \n --details --reason [--kind ]\n [--tag ]... [--importance <0-100>] [--pinned] [--json]\n bb memory update --expected-version [fields...] [--json]\n bb memory forget --expected-version --reason [--json]\n bb memory history [--scope project|global|all] [--limit 1-100] [--json]\n\nProject writes use the invoking CLI's current project. Global writes require\nthe explicit `--scope global` flag.\n\nThe Docs plugin is an opt-in official plugin bundled with the app:\n`bb plugin install docs`. Read-only discovery remains direct, while edits use\na manifest-backed local workspace:\n\n bb docs vaults [--json]\n bb docs list [--vault ] [--json]\n bb docs read [--vault ]\n bb docs pull [--folder] [--vault ] [--into ]\n bb docs pull --all [--vault ] [--into ]\n bb docs status [workspace-dir] [--delete] [--diff] [--json]\n bb docs push [workspace-dir] [--delete] [--dry-run] [--diff] [--json]\n\nPull preserves vault-relative paths and writes `.bb-docs-state.json`; edit the\nordinary files and leave that state file untouched. Push uses pulled SHA-256\nversions as compare-and-swap guards. Concurrent changes stop with exit 3.\nLocal file and empty-directory deletions are warnings unless `--delete` is\nexplicit; a pulled folder root is retained, so pull its parent or the whole\nvault to remove that folder. Use `--workspace-host ` when a standalone\nCLI's working directory is on a non-primary host. Direct `write`, `mkdir`,\n`move`, and `remove` remain only as deprecated compatibility commands.\n\nThe Tasks plugin is an opt-in official plugin bundled with the app:\n`bb plugin install tasks`. It adds a task tracker, agent delegation,\nand the `bb tasks` command. Common agent operations are:\n\n bb tasks show [--json]\n bb tasks list [--project ] [filters...] [--sort manual|priority|due] [--limit 1-500] [--cursor ] [--json]\n bb tasks comment (--body | --body-file ) [--json]\n bb tasks attachment add --file [--json]\n bb tasks attachment get --out [--json]\n bb tasks attach [--json]\n bb tasks update --status in_review [--json]\n bb tasks update (--parent | --no-parent) [--json]\n\nRun `bb tasks --help` for project, folder, task, label, attachment, and demo-data\ncommands, plus preset management, delegation, and attached-thread inspection.\nDelegated threads are attached automatically; use `bb tasks attach` only when\nwork started outside Tasks. Task update resolves both task keys and IDs for\n`--parent`; use `--no-parent` to promote a subtask to the top level. File paths\nin tasks commands resolve on the invoking machine (the thread's machine inside\nan agent thread, otherwise the server's); pass `--machine ` to\ntarget another enrolled machine.\nTask lists default to 100 rows. JSON pages include `nextCursor`; human pages\nprint the exact continuation option when more rows exist. Cursors are bound to\nthe filters, sort, and task-list revision. Any add, removal, reorder, update,\nlabel-link/name change, active-thread change, or project-prefix change invalidates an\noutstanding cursor; restart without `--cursor` instead of accepting a mixed\nsnapshot.\n\nThe builtin Secrets plugin provides a secure credential form and guarded\ndotenv reconciliation:\n\n bb secret request --write-env \n [--purpose ] [--describe ]...\n\nThe command blocks until the user submits or cancels the form. Secret values\nnever appear in command arguments, model-visible output, or persisted\ninteraction data; success prints only the path, variable names, and\nadded/updated/unchanged counts.\n\n bb plugin search Search BB's official plugins (bundled with\n the app)\n bb plugin install Install a bundled official plugin by name\n (github, docs, memory, tasks,\n t3sidebar), a local\n path, builtin:,\n git:@, or\n npm:[@]\n (npm: needs npm on PATH; installs prompt —\n pass --yes to skip). Managed git:/npm:\n installs refuse engines.bb / engines.bbPluginSdk\n mismatches, manifest/artifact identity\n mismatches, and ids reserved by bundled plugins\n Omitted npm specs, ranges, dist-tags, and git\n branches track; exact npm versions, git tags,\n and git commits are pinned\n bb plugin outdated Check installed plugins for compatible\n updates (table; --json for raw results).\n Columns: installed, latest compatible,\n blocked newer (incompatible releases not\n selected), status. Dev builds (bb 0.0.0)\n annotate that engines.bb is not enforced\n bb plugin update | --all Apply compatible updates for one plugin or\n every tracking plugin with an update. Same\n full-trust confirmation as\n install (--yes skips; non-TTY refuses without\n --yes). Use outdated to preview; pinned\n installs stay put\n bb plugin list Status, services, schedules, handler timings\n bb plugin source [--json] Show requested/resolved source, engine ranges,\n install time, and recent activation history\n bb plugin enable|disable Load or unload an installed plugin\n bb plugin reload [id] Re-run factories against current sources\n bb plugin config [set | unset ]\n Show or change a plugin's declared settings\n bb plugin logs [-n N] [-f] Print (or follow) a plugin's bb.log output\n bb plugin run [args...] Run the plugin's CLI command explicitly\n bb plugin token [--rotate] Print the token for auth:\"token\" HTTP\n routes; --rotate generates a new token,\n invalidating the old one\n bb plugin remove Uninstall (managed git:/npm: files deleted;\n builtin removals are remembered)\n bb plugin new [--app] Scaffold a new plugin (no server required;\n --app adds a frontend entry, app.tsx, plus a\n typecheck-only tsconfig.json)\n bb plugin build [path] Compile the plugin into dist/ — the backend\n bundle (server.js, server.meta.json) and,\n when bb.app is declared, the frontend bundle\n (app.js, app.css, app.meta.json). Each\n *.meta.json is stamped with SDK major/version,\n artifactFormatVersion, pluginId, pluginVersion,\n and builtWith (bb + plugin SDK versions); no\n server required\n bb plugin dev [path] Watch a plugin's sources (default: cwd) and\n on every change rebuild its frontend bundle\n (if it declares bb.app) and reload the\n plugin; Ctrl+C to stop\n\nBB Official plugins\n\nBB's official plugins — GitHub, Docs, Memory, Tasks, and T3 Sidebar — ship\nbundled inside the app itself. They appear in Extensions → Plugins → Browse\nand install with one click from the local bundled copy: no network, no\ndownload, no separate release. Install from the CLI by bare name\n(`bb plugin install github`, `bb plugin install docs`,\n`bb plugin install memory`, `bb plugin install tasks`, or\n`bb plugin install t3sidebar`). Installed official plugins are pinned to the\nbundled copy and update automatically when the BB app updates.\n\nFor direct git:/npm: installs, updates are manual: `bb plugin outdated`\nchecks tracking sources and `bb plugin update` applies compatible candidates.\nReinstalling an already-installed managed plugin is refused — use\n`bb plugin update`. A failed activation restores the pre-update snapshot and\nleaves the latest failure visible as needing attention. Exact npm versions,\ngit tags and commits, path sources, and bundled official plugins are pinned;\nnpm ranges/omitted specs/dist-tags and git branches track compatible updates.\n\n`bb plugin search ` matches id, display name, description, and\ncategory across the bundled official plugins (status: installed / compatible\n/ requires newer bb). Install an official plugin by its bare name. Direct\n`path:`, `npm:`, `git:`, and `builtin:` sources—and path-like\nsyntax—continue to bypass official-plugin resolution.\n\nBuilds are automatic once installed. Git installs run `npm install`\n(lifecycle scripts disabled), then compile both bundles — so a git plugin may\ndepend on third-party packages. node_modules is kept, because bundling cannot\ninline data files a dependency reads at runtime. A committed dist/ is always\nreplaced by the bundles bb builds. Path installs compile dist/ at install time\nfrom dependencies you have already installed. A build failure fails the\ninstall. npm packages must ship a metadata-validated prebuilt app or the\ninstall is refused. The server rebuilds source-built apps after a bb upgrade.\n\nInstalling or updating a git plugin requires `npm` on PATH. Checking for\nupdates does not: a check reads the candidate's manifest and stops, so\npolling never resolves a dependency tree or builds. A candidate that fails to\nbuild is reported as available and fails when you apply it.\n\nbb ships no build toolchain. The first time a git or path plugin is built on\na machine, bb downloads a pinned esbuild + Tailwind set into\n`/plugins/toolchain-/` and reuses it afterwards. Installing\na prebuilt npm plugin never triggers that download.\n\nTo build a plugin yourself — in CI, or to check it compiles without a running\nbb — depend on the published `bb-app` package and call the CLI:\n\n```jsonc\n// your plugin's package.json\n\"devDependencies\": { \"bb-app\": \"^0.35.1\" },\n\"scripts\": { \"build\": \"bb plugin build\" }\n```\n\n`bb plugin build` talks to no server. Depending on `bb-app@X` builds with\nexactly that release's shim configuration, so the bundle cannot be built\nagainst a mismatched host runtime. Cache the toolchain directory in CI to skip\nthe download on later runs. Only `bb plugin dev` needs a running bb, because\nit reloads the installed plugin after each rebuild.\n\nThe backend half is prebuilt too: when a builtin/official/git/npm install\nships a dist/server.js built for the running SDK major, the server loads it\ninstead of the TypeScript source. Path installs always load server.ts from\nsource, so `bb plugin dev`/reload see edits immediately.\n\n`bb plugin dev` is the edit loop: it requires the directory to already be\ninstalled as a plugin (`bb plugin install .` first), ignores dist/,\nnode_modules/, and .git/, batches saves, and prints one line per cycle. A\nbuild or reload failure prints the error and keeps watching (a failed build\nskips that cycle's reload). Reloads reach open app pages live — changed\nfrontend bundles re-import and their UI slots remount without a page\nrefresh.\n\nFrontend entries (app.tsx) default-export `definePluginApp` from\n`@bb/plugin-sdk/app` and register UI slots: homepageSection (root compose),\nsettingsSection (per-plugin settings page below the host-rendered settings\nform; no props in V1, optional host-rendered title),\nnavPanel (own sidebar entry + /plugins///* route; the remainder\narrives as the component's subPath prop for panel-internal deep links; the\nhost always renders the shared plugin title bar and the component owns a\nzero-padding full-bleed body, including its scrolling),\nthreadPanelAction\n(an entry in the thread right panel's new-tab Actions list whose run() can\nopen closable panel tabs with recursive `JsonValue` params; restored\ncomponents read `JsonValue | null`), pendingInteraction (temporarily replace a thread composer with a\nplugin form), fileOpener (register as a per-extension file viewer/editor;\nusers pick defaults under Settings → File openers and can right-click a\nfile link for a one-off choice), and messageDirective (replace a leaf\n`::name{k=\"v\"}` block inside assistant / nested-agent Markdown with a plugin\ncomponent; unknown, disabled, incomplete, code-fenced, or crashing\ndirectives fall back to the original source; components receive a nullable\nopenWorkspaceFile(path) callback for opening a worktree-relative file in the\nhost workspace viewer and a nullable\nopenThreadPanel({ actionId, title?, params? }) callback for opening one of the\nsame plugin's thread-panel actions). Hooks:\nuseRpc, useRealtime, useRealtimeConnectionState (the shared realtime socket's\nconnecting/connected/reconnecting lifecycle; reconcile on later connected\ntransitions, not the initial connection), useSettings (secrets excluded),\nuseBbContext,\nuseBbNavigate, useComposer (read/replace/update/clear scoped composer text,\napply a class-based text effect, lock input, quote selections, insert mention\npills, and focus the composer), and useComposerView (reactive bound scope,\nlayout, draft, and run state). Plain-text edits preserve attachments and\nreconcile only inline mentions overlapped by the edit. Define RPC methods with `defineRpcContract`\nand Standard Schema-compatible input/output validators (Zod works directly),\nregister via `bb.rpc.register(contract, handlers)`, then use a type-only\nbackend contract import with `useRpc()` for exact frontend\nmethod/input/result inference. The server validates both schemas and rejects\nnon-JSON results (including cyclic and non-finite values) with structured\nerror codes. Components are vendored shadcn source the plugin owns (the\nshadcn model): `bb plugin new --app` pre-vendors a starter set into\ncomponents/ui/ and `npx shadcn add @bb/` pulls more from the BB\ncomponent registry (the full stock shadcn set, version-matched to the\nrunning BB via the pinned ref in components.json). `import { toast } from\n\"sonner\"` reaches the host toaster; react, the portaling radix families,\nsonner, vaul, and @pierre/diffs (the app's syntax-highlighted diff\nrenderer) are runtime-shimmed (never bundled), everything else\nbundles from the plugin's node_modules (`npm install` for authors; BB installs\nrelease packages with their declared production dependencies). A crashing slot collapses to a\n\"plugin crashed\" chip without\ntouching the rest of the app. Installed plugins and their declared settings\n(same data as `bb plugin config`) also appear under Extensions → Plugins.\n\nPlugin CLI commands: a plugin can register one top-level subcommand (for\nexample `bb github …`). Unknown `bb` commands are looked up against installed\nplugins and proxied to the server, so plugin commands work exactly like core\ncommands; core command names always win. Inside agent threads the generated\n`plugin-commands` skill lists the available plugin commands.\n\nSettings changes do not auto-reload a plugin — run `bb plugin reload `\nafter configuring. Add --json to plugin commands for machine-readable output.\nPlugin CLI stdout plus stderr is capped at 1,048,576 UTF-8 bytes from the\nshared `@bb/plugin-sdk` constant. Results above the ceiling are rejected in\nfull with a structured `plugin_cli_output_too_large` error; output is never\nsilently clipped. Page growing collections and use file/streaming commands for\nlarge content.\n\nAuthoring a plugin\n\nThe loop: `bb plugin new ` scaffolds `./bb-plugin-` (add --app\nfor a frontend entry); `bb plugin install .` registers it; `bb plugin dev`\nwatches and reloads on every save. The manifest is package.json: required\n`bb.name` and `bb.description` human identity, required `bb.branding` with at\nleast `icon` or `logo.light`, `bb.server`\n(backend entry, loaded as TypeScript — no build step), optional `bb.app`\n(frontend entry), optional `bb.skills` (static skill directories auto-imported\ninto agent threads unless filtered by `bb.agents.configure`; default\n`skills/`), `engines.bb` (supported bb range),\nand optional `engines.bbPluginSdk` (supported plugin SDK range; scaffold\nwrites `\"^0.4.1\"` for SDK 0.4.1). The plugin id is the package name minus\n`bb-plugin-`.\n\nPlugins can contribute palettes with `bb.themes`: an array of\n`{ id, name, description?, css }`, where `css` is a plugin-relative `.css`\nfile. Loaded plugin palettes appear in Settings → Appearance and `bb theme\nlist`; their selectable id is `plugin::`. Disabling or\nremoving the owning plugin makes bb fall back to the default palette.\n\nBranding is explicit. Declare `bb.branding.icon` as either the plugin's\ncanonical BB icon name or a plugin-relative compact SVG such as\n`./assets/icon.svg`. BB validates and hash-serves path-shaped SVGs, then\nrenders them as masks that inherit the surrounding text color. Compact chrome\nprefers the manifest icon, then a contribution's local icon hint, and finally\nZap. Roomy surfaces reuse the same icon when no logo override is declared.\n\nAdd `bb.branding.logo.light` only for intentionally different rich/full-size\nidentity artwork; optional `bb.branding.logo.dark` is preferred in dark mode.\nLogo paths must be plugin-relative `.svg`, `.png`, or `.webp` files. Root logo\nfiles are not auto-detected, and a dark logo requires a light logo. Logo-only\nmanifests remain supported for compatibility, so at least an icon or light logo\nis required. Do not duplicate the same artwork across fields. BB rejects nulls,\nempty strings, missing or escaping assets, and unsupported extensions. Reload\nthe plugin to pick up branding changes.\n\nThe backend entry default-exports a factory receiving the full plugin API:\n\n import type { BbPluginApi } from \"@bb/plugin-sdk\";\n export default async function plugin(bb: BbPluginApi) { ... }\n\nThe import is type-only and erased at load; the scaffold ships the full API\nas bundled .d.ts in types/ (tsconfig maps @bb/plugin-sdk to them), so\n`npm install && npx tsc --noEmit` typechecks anywhere — no bb checkout\nneeded. Confused, or need a symbol the types don't explain? Clone the repo:\nhttps://github.com/get-bb/bb. The API in\none line each — bb.log (plugin-scoped logger behind `bb plugin logs`);\nbb.settings.define (declarative settings incl. secrets, editable via\n`bb plugin config`); bb.storage.kv (JSON rows ≤256KB) and\nbb.storage.database()+migrate (the plugin's own database); bb.sdk (the full\nbb SDK — handlers/services only, not the factory; spawned threads are\nattributed to the plugin; `visibility: \"hidden\"` creates directly addressable\nbackground workers omitted from sidebar organization and unread/pending\nfavicon attention, with other behavior unchanged; a child thread inherits\nits parent's visibility and still notifies that parent);\nbb.events.on (observe thread.created/idle/failed/deleted);\nbb.http.route (routes under /api/v1/plugins//http/* with\nlocal/token/none auth); defineRpcContract + bb.rpc.register (Standard\nSchema-validated frontend data plane with inferred backend handlers and\ntype-only frontend method/input/result inference);\nbb.realtime.publish (ephemeral signals to open app pages);\nbb.background.service (long-lived, AbortSignal, restart w/ backoff) and\nbb.background.schedule (durable cron rows); bb.cli.register (a top-level\n`bb ` command agents run through bash, with a shared 1 MiB combined\nstdout/stderr ceiling and atomic structured over-limit errors); bb.agents.registerTool\n(static native tools with zod or JSON-schema parameters) and\nbb.agents.configure (one synchronous per-resolution callback selecting this\nplugin's own tool/skill ids and optional dynamic instructions; tools apply on\nthe next provider session start/resume, while busy skill runtimes defer catalog\nchanges); bb.ui\nregisterMentionProvider (host-rendered UI — no\nfrontend bundle needed); bb.status.needsConfiguration (report\n\"unconfigured\" instead of crashing); bb.onDispose (LIFO cleanup on\nreload/disable/shutdown).\n\nFrontend entries register React slots (homepageSection, settingsSection,\nnavPanel, threadPanelAction, fileOpener, messageDirective) and composer\ncustomizations via `app.composer.customize({ actions, plusMenu, banners,\nrichText })`; action/banner components use `useComposer()` and\n`useComposerView()`, while the host renders plus-menu rows and editor\ndecorations. The deprecated pre-1.0 `slots.composerAccessory` footer API was\nremoved; migrate controls to actions or the plus menu and larger content to\nbanners. Register all frontend surfaces via\ndefinePluginApp, use the hooks\nlisted above, and render vendored components; styling is Tailwind against\nthe host theme's tokens only (semantic classes like bg-background and\ntw-animate-css utilities compile in plugin builds).\n\nFor the complete authoring reference — exact signatures, working snippets\nfor every surface, the reload lifecycle, testing tips, and gotchas — use\nthe built-in `bb-plugin-authoring` skill (agents: it loads on demand;\nhumans: apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/\nin a checkout). The builtin `inline-vis` plugin renders\n`::inline-vis{file=\"demo.html\" height=\"480\"}` through the sidebar's\npath-shaped, sandboxed worktree HTML iframe preview; `height` is optional.\nIts card header includes an open-in-sidebar action for the source HTML file.\nThe `plugins/` directory contains every bundled plugin: the auto-installed\nbuiltins and the store-only BB Official GitHub, Docs, Memory, Tasks, and T3\nSidebar plugins. The `examples/plugins/` reference plugins\ncover slack-bot (webhook bot), agent-enrichment (agent surfaces), and\ncomposer-customization (all composer regions).", + "body": "Plugin commands\n\nA bb plugin is a TypeScript package that extends the bb server in-process:\nbackground services, cron schedules, HTTP/RPC endpoints, thread lifecycle\nhandlers, settings, storage — and `bb` CLI subcommands that agents and humans\nrun like any other command. Plugins are full-trust code inside the server.\n\nPlugins are on by default. Builtin plugins (`builtin:`) ship with bb;\nuser-installed plugins come from `bb plugin install` or the official store.\nPlugin state lives under `/plugins//` (per-plugin SQLite file,\nsecrets, logs).\n\nThe builtin Custom instructions plugin adds a multiline editor under Settings\n→ Custom instructions. Saved text is persisted on this bb host and included in\nagent task instructions; blank text contributes nothing.\n\nThe builtin Workflows plugin runs durable provider-independent JavaScript\norchestration. It is disabled on fresh installations; enable `workflows` under\nExtensions → Plugins or run `bb plugin enable workflows` before using:\n\n bb workflows validate (--script ''|--source ''|\n --file |--name )\n bb workflows run (--script ''|--source ''|\n --file |--name )\n [--args ''] [--resume ]\n bb workflows status \n bb workflows history [--cursor ] [--limit <1-100>]\n bb workflows list [--limit <1-50>]\n bb workflows stop \n\nCommands must run from a BB project thread. Workflows has six plugin\nsettings, configurable with `bb plugin config workflows set `:\n`maxActiveRuns` (default 4, range 1–32), `maxConcurrentAgents` (8, 1–64),\n`maxAgentCalls` (100, 1–1000), `totalRunTimeoutMs` (86400000, 60000–604800000),\n`retentionDays` (30, 1–3650), and `maxNotificationBytes` (16384,\n1024–262144). `maxActiveRuns` applies live; the other five are snapshotted for\neach new run. Settings changes do not require a plugin reload.\n\n`status` is a bounded polling summary, and `list` returns only compact run\nsummaries. Detailed run and call records are paged JSONL: redirect `history`\ninto `$BB_THREAD_STORAGE` before inspecting it, and continue with the final\npage record's `nextCursor`. The invoking shell writes\nthat file on the thread's execution host, so this works the same on local and\nremote hosts without granting the plugin arbitrary filesystem access. Use `bb\nprovider list --environment \"$BB_ENVIRONMENT_ID\" --json` and then `bb provider\nmodels --environment \"$BB_ENVIRONMENT_ID\" --json` before writing\nan explicit selection; never guess ACP model IDs.\n\nThe Memory plugin is an opt-in install, bundled with the app:\n`bb plugin install memory`. Once installed, it injects a compact global and\ncurrent-project memory index into agent context and progressively discloses\nfull records through CLI-only commands. Because its store works across\nproviders, we recommend disabling provider-native memory under Settings →\nProviders to avoid duplicate or conflicting stores. Settings → Memory lists\nevery global and project memory and supports version-checked edits and soft\ndeletion.\n\n bb memory catalog [--scope project|global|all] [--json]\n bb memory search [--scope project|global|all] [--json]\n bb memory get [--scope project|global|all] [--json]\n bb memory add --scope project|global --name --summary \n --details --reason [--kind ]\n [--tag ]... [--importance <0-100>] [--pinned] [--json]\n bb memory update --expected-version [fields...] [--json]\n bb memory forget --expected-version --reason [--json]\n bb memory history [--scope project|global|all] [--limit 1-100] [--json]\n\nProject writes use the invoking CLI's current project. Global writes require\nthe explicit `--scope global` flag.\n\nThe Docs plugin is an opt-in official plugin bundled with the app:\n`bb plugin install docs`. Read-only discovery remains direct, while edits use\na manifest-backed local workspace:\n\n bb docs vaults [--json]\n bb docs list [--vault ] [--json]\n bb docs read [--vault ]\n bb docs pull [--folder] [--vault ] [--into ]\n bb docs pull --all [--vault ] [--into ]\n bb docs status [workspace-dir] [--delete] [--diff] [--json]\n bb docs push [workspace-dir] [--delete] [--dry-run] [--diff] [--json]\n\nPull preserves vault-relative paths and writes `.bb-docs-state.json`; edit the\nordinary files and leave that state file untouched. Push uses pulled SHA-256\nversions as compare-and-swap guards. Concurrent changes stop with exit 3.\nLocal file and empty-directory deletions are warnings unless `--delete` is\nexplicit; a pulled folder root is retained, so pull its parent or the whole\nvault to remove that folder. Use `--workspace-host ` when a standalone\nCLI's working directory is on a non-primary host. Direct `write`, `mkdir`,\n`move`, and `remove` remain only as deprecated compatibility commands.\n\nThe Tasks plugin is an opt-in official plugin bundled with the app:\n`bb plugin install tasks`. It adds a task tracker, agent delegation,\nand the `bb tasks` command. Common agent operations are:\n\n bb tasks show [--json]\n bb tasks list [--project ] [filters...] [--sort manual|priority|due] [--limit 1-500] [--cursor ] [--json]\n bb tasks comment (--body | --body-file ) [--json]\n bb tasks attachment add --file [--json]\n bb tasks attachment get --out [--json]\n bb tasks attach [--json]\n bb tasks update --status in_review [--json]\n bb tasks update (--parent | --no-parent) [--json]\n\nRun `bb tasks --help` for project, folder, task, label, attachment, and demo-data\ncommands, plus preset management, delegation, and attached-thread inspection.\nDelegated threads are attached automatically; use `bb tasks attach` only when\nwork started outside Tasks. Task update resolves both task keys and IDs for\n`--parent`; use `--no-parent` to promote a subtask to the top level. File paths\nin tasks commands resolve on the invoking machine (the thread's machine inside\nan agent thread, otherwise the server's); pass `--machine ` to\ntarget another enrolled machine.\nTask lists default to 100 rows. JSON pages include `nextCursor`; human pages\nprint the exact continuation option when more rows exist. Cursors are bound to\nthe filters, sort, and task-list revision. Any add, removal, reorder, update,\nlabel-link/name change, active-thread change, or project-prefix change invalidates an\noutstanding cursor; restart without `--cursor` instead of accepting a mixed\nsnapshot.\n\nThe builtin Secrets plugin provides a secure credential form and guarded\ndotenv reconciliation:\n\n bb secret request --write-env \n [--purpose ] [--describe ]...\n\nThe command blocks until the user submits or cancels the form. Secret values\nnever appear in command arguments, model-visible output, or persisted\ninteraction data; success prints only the path, variable names, and\nadded/updated/unchanged counts.\n\n bb plugin search Search BB's official plugins (bundled with\n the app)\n bb plugin install Install a bundled official plugin by name\n (github, docs, memory, tasks), a local\n path, builtin:,\n git:@, or\n npm:[@]\n (npm: needs npm on PATH; installs prompt —\n pass --yes to skip). Managed git:/npm:\n installs refuse engines.bb / engines.bbPluginSdk\n mismatches, manifest/artifact identity\n mismatches, and ids reserved by bundled plugins\n Omitted npm specs, ranges, dist-tags, and git\n branches track; exact npm versions, git tags,\n and git commits are pinned\n bb plugin outdated Check installed plugins for compatible\n updates (table; --json for raw results).\n Columns: installed, latest compatible,\n blocked newer (incompatible releases not\n selected), status. Dev builds (bb 0.0.0)\n annotate that engines.bb is not enforced\n bb plugin update | --all Apply compatible updates for one plugin or\n every tracking plugin with an update. Same\n full-trust confirmation as\n install (--yes skips; non-TTY refuses without\n --yes). Use outdated to preview; pinned\n installs stay put\n bb plugin list Status, services, schedules, handler timings\n bb plugin source [--json] Show requested/resolved source, engine ranges,\n install time, and recent activation history\n bb plugin enable|disable Load or unload an installed plugin\n bb plugin reload [id] Re-run factories against current sources\n bb plugin config [set | unset ]\n Show or change a plugin's declared settings\n bb plugin logs [-n N] [-f] Print (or follow) a plugin's bb.log output\n bb plugin run [args...] Run the plugin's CLI command explicitly\n bb plugin token [--rotate] Print the token for auth:\"token\" HTTP\n routes; --rotate generates a new token,\n invalidating the old one\n bb plugin remove Uninstall (managed git:/npm: files deleted;\n builtin removals are remembered)\n bb plugin new [--app] Scaffold a new plugin (no server required;\n --app adds a frontend entry, app.tsx, plus a\n typecheck-only tsconfig.json)\n bb plugin build [path] Compile the plugin into dist/ — the backend\n bundle (server.js, server.meta.json) and,\n when bb.app is declared, the frontend bundle\n (app.js, app.css, app.meta.json). Each\n *.meta.json is stamped with SDK major/version,\n artifactFormatVersion, pluginId, pluginVersion,\n and builtWith (bb + plugin SDK versions); no\n server required\n bb plugin dev [path] Watch a plugin's sources (default: cwd) and\n on every change rebuild its frontend bundle\n (if it declares bb.app) and reload the\n plugin; Ctrl+C to stop\n\nBB Official plugins\n\nBB's official plugins — GitHub, Docs, Memory, and Tasks — ship bundled inside\nthe app itself. They appear in Extensions → Plugins → Browse\nand install with one click from the local bundled copy: no network, no\ndownload, no separate release. Install from the CLI by bare name\n(`bb plugin install github`, `bb plugin install docs`, `bb plugin install\nmemory`, or `bb plugin install tasks`). Installed official plugins are pinned\nto the bundled copy and update automatically when the BB app updates.\n\nFor direct git:/npm: installs, updates are manual: `bb plugin outdated`\nchecks tracking sources and `bb plugin update` applies compatible candidates.\nReinstalling an already-installed managed plugin is refused — use\n`bb plugin update`. A failed activation restores the pre-update snapshot and\nleaves the latest failure visible as needing attention. Exact npm versions,\ngit tags and commits, path sources, and bundled official plugins are pinned;\nnpm ranges/omitted specs/dist-tags and git branches track compatible updates.\n\n`bb plugin search ` matches id, display name, description, and\ncategory across the bundled official plugins (status: installed / compatible\n/ requires newer bb). Install an official plugin by its bare name. Direct\n`path:`, `npm:`, `git:`, and `builtin:` sources—and path-like\nsyntax—continue to bypass official-plugin resolution.\n\nBuilds are automatic once installed. Git installs run `npm install`\n(lifecycle scripts disabled), then compile both bundles — so a git plugin may\ndepend on third-party packages. node_modules is kept, because bundling cannot\ninline data files a dependency reads at runtime. A committed dist/ is always\nreplaced by the bundles bb builds. Path installs compile dist/ at install time\nfrom dependencies you have already installed. A build failure fails the\ninstall. npm packages must ship a metadata-validated prebuilt app or the\ninstall is refused. The server rebuilds source-built apps after a bb upgrade.\n\nInstalling or updating a git plugin requires `npm` on PATH. Checking for\nupdates does not: a check reads the candidate's manifest and stops, so\npolling never resolves a dependency tree or builds. A candidate that fails to\nbuild is reported as available and fails when you apply it.\n\nbb ships no build toolchain. The first time a git or path plugin is built on\na machine, bb downloads a pinned esbuild + Tailwind set into\n`/plugins/toolchain-/` and reuses it afterwards. Installing\na prebuilt npm plugin never triggers that download.\n\nTo build a plugin yourself — in CI, or to check it compiles without a running\nbb — depend on the published `bb-app` package and call the CLI:\n\n```jsonc\n// your plugin's package.json\n\"devDependencies\": { \"bb-app\": \"^0.35.1\" },\n\"scripts\": { \"build\": \"bb plugin build\" }\n```\n\n`bb plugin build` talks to no server. Depending on `bb-app@X` builds with\nexactly that release's shim configuration, so the bundle cannot be built\nagainst a mismatched host runtime. Cache the toolchain directory in CI to skip\nthe download on later runs. Only `bb plugin dev` needs a running bb, because\nit reloads the installed plugin after each rebuild.\n\nThe backend half is prebuilt too: when a builtin/official/git/npm install\nships a dist/server.js built for the running SDK major, the server loads it\ninstead of the TypeScript source. Path installs always load server.ts from\nsource, so `bb plugin dev`/reload see edits immediately.\n\n`bb plugin dev` is the edit loop: it requires the directory to already be\ninstalled as a plugin (`bb plugin install .` first), ignores dist/,\nnode_modules/, and .git/, batches saves, and prints one line per cycle. A\nbuild or reload failure prints the error and keeps watching (a failed build\nskips that cycle's reload). Reloads reach open app pages live — changed\nfrontend bundles re-import and their UI slots remount without a page\nrefresh.\n\nFrontend entries (app.tsx) default-export `definePluginApp` from\n`@bb/plugin-sdk/app` and register UI slots: homepageSection (root compose),\nsettingsSection (per-plugin settings page below the host-rendered settings\nform; no props in V1, optional host-rendered title),\nnavPanel (own sidebar entry + /plugins///* route; the remainder\narrives as the component's subPath prop for panel-internal deep links; the\nhost always renders the shared plugin title bar and the component owns a\nzero-padding full-bleed body, including its scrolling),\nthreadPanelAction\n(an entry in the thread right panel's new-tab Actions list whose run() can\nopen closable panel tabs with recursive `JsonValue` params; restored\ncomponents read `JsonValue | null`), pendingInteraction (temporarily replace a thread composer with a\nplugin form), fileOpener (register as a per-extension file viewer/editor;\nusers pick defaults under Settings → File openers and can right-click a\nfile link for a one-off choice), and messageDirective (replace a leaf\n`::name{k=\"v\"}` block inside assistant / nested-agent Markdown with a plugin\ncomponent; unknown, disabled, incomplete, code-fenced, or crashing\ndirectives fall back to the original source; components receive a nullable\nopenWorkspaceFile(path) callback for opening a worktree-relative file in the\nhost workspace viewer and a nullable\nopenThreadPanel({ actionId, title?, params? }) callback for opening one of the\nsame plugin's thread-panel actions). Hooks:\nuseRpc, useRealtime, useRealtimeConnectionState (the shared realtime socket's\nconnecting/connected/reconnecting lifecycle; reconcile on later connected\ntransitions, not the initial connection), useSettings (secrets excluded),\nuseBbContext,\nuseBbNavigate, useComposer (read/replace/update/clear scoped composer text,\napply a class-based text effect, lock input, quote selections, insert mention\npills, and focus the composer), and useComposerView (reactive bound scope,\nlayout, draft, and run state). Plain-text edits preserve attachments and\nreconcile only inline mentions overlapped by the edit. Define RPC methods with `defineRpcContract`\nand Standard Schema-compatible input/output validators (Zod works directly),\nregister via `bb.rpc.register(contract, handlers)`, then use a type-only\nbackend contract import with `useRpc()` for exact frontend\nmethod/input/result inference. The server validates both schemas and rejects\nnon-JSON results (including cyclic and non-finite values) with structured\nerror codes. Components are vendored shadcn source the plugin owns (the\nshadcn model): `bb plugin new --app` pre-vendors a starter set into\ncomponents/ui/ and `npx shadcn add @bb/` pulls more from the BB\ncomponent registry (the full stock shadcn set, version-matched to the\nrunning BB via the pinned ref in components.json). `import { toast } from\n\"sonner\"` reaches the host toaster; react, the portaling radix families,\nsonner, vaul, and @pierre/diffs (the app's syntax-highlighted diff\nrenderer) are runtime-shimmed (never bundled), everything else\nbundles from the plugin's node_modules (`npm install` for authors; BB installs\nrelease packages with their declared production dependencies). A crashing slot collapses to a\n\"plugin crashed\" chip without\ntouching the rest of the app. Installed plugins and their declared settings\n(same data as `bb plugin config`) also appear under Extensions → Plugins.\n\nPlugin CLI commands: a plugin can register one top-level subcommand (for\nexample `bb github …`). Unknown `bb` commands are looked up against installed\nplugins and proxied to the server, so plugin commands work exactly like core\ncommands; core command names always win. Inside agent threads the generated\n`plugin-commands` skill lists the available plugin commands.\n\nSettings changes do not auto-reload a plugin — run `bb plugin reload `\nafter configuring. Add --json to plugin commands for machine-readable output.\nPlugin CLI stdout plus stderr is capped at 1,048,576 UTF-8 bytes from the\nshared `@bb/plugin-sdk` constant. Results above the ceiling are rejected in\nfull with a structured `plugin_cli_output_too_large` error; output is never\nsilently clipped. Page growing collections and use file/streaming commands for\nlarge content.\n\nAuthoring a plugin\n\nThe loop: `bb plugin new ` scaffolds `./bb-plugin-` (add --app\nfor a frontend entry); `bb plugin install .` registers it; `bb plugin dev`\nwatches and reloads on every save. The manifest is package.json: required\n`bb.name` and `bb.description` human identity, required `bb.branding` with at\nleast `icon` or `logo.light`, `bb.server`\n(backend entry, loaded as TypeScript — no build step), optional `bb.app`\n(frontend entry), optional `bb.skills` (static skill directories auto-imported\ninto agent threads unless filtered by `bb.agents.configure`; default\n`skills/`), `engines.bb` (supported bb range),\nand optional `engines.bbPluginSdk` (supported plugin SDK range; scaffold\nwrites `\"^0.4.1\"` for SDK 0.4.1). The plugin id is the package name minus\n`bb-plugin-`.\n\nPlugins can contribute palettes with `bb.themes`: an array of\n`{ id, name, description?, css }`, where `css` is a plugin-relative `.css`\nfile. Loaded plugin palettes appear in Settings → Appearance and `bb theme\nlist`; their selectable id is `plugin::`. Disabling or\nremoving the owning plugin makes bb fall back to the default palette.\n\nBranding is explicit. Declare `bb.branding.icon` as either the plugin's\ncanonical BB icon name or a plugin-relative compact SVG such as\n`./assets/icon.svg`. BB validates and hash-serves path-shaped SVGs, then\nrenders them as masks that inherit the surrounding text color. Compact chrome\nprefers the manifest icon, then a contribution's local icon hint, and finally\nZap. Roomy surfaces reuse the same icon when no logo override is declared.\n\nAdd `bb.branding.logo.light` only for intentionally different rich/full-size\nidentity artwork; optional `bb.branding.logo.dark` is preferred in dark mode.\nLogo paths must be plugin-relative `.svg`, `.png`, or `.webp` files. Root logo\nfiles are not auto-detected, and a dark logo requires a light logo. Logo-only\nmanifests remain supported for compatibility, so at least an icon or light logo\nis required. Do not duplicate the same artwork across fields. BB rejects nulls,\nempty strings, missing or escaping assets, and unsupported extensions. Reload\nthe plugin to pick up branding changes.\n\nThe backend entry default-exports a factory receiving the full plugin API:\n\n import type { BbPluginApi } from \"@bb/plugin-sdk\";\n export default async function plugin(bb: BbPluginApi) { ... }\n\nThe import is type-only and erased at load; the scaffold ships the full API\nas bundled .d.ts in types/ (tsconfig maps @bb/plugin-sdk to them), so\n`npm install && npx tsc --noEmit` typechecks anywhere — no bb checkout\nneeded. Confused, or need a symbol the types don't explain? Clone the repo:\nhttps://github.com/get-bb/bb. The API in\none line each — bb.log (plugin-scoped logger behind `bb plugin logs`);\nbb.settings.define (declarative settings incl. secrets, editable via\n`bb plugin config`); bb.storage.kv (JSON rows ≤256KB) and\nbb.storage.database()+migrate (the plugin's own database); bb.sdk (the full\nbb SDK — handlers/services only, not the factory; spawned threads are\nattributed to the plugin; `visibility: \"hidden\"` creates directly addressable\nbackground workers omitted from sidebar organization and unread/pending\nfavicon attention, with other behavior unchanged; a child thread inherits\nits parent's visibility and still notifies that parent);\nbb.events.on (observe thread.created/idle/failed/deleted);\nbb.http.route (routes under /api/v1/plugins//http/* with\nlocal/token/none auth); defineRpcContract + bb.rpc.register (Standard\nSchema-validated frontend data plane with inferred backend handlers and\ntype-only frontend method/input/result inference);\nbb.realtime.publish (ephemeral signals to open app pages);\nbb.background.service (long-lived, AbortSignal, restart w/ backoff) and\nbb.background.schedule (durable cron rows); bb.cli.register (a top-level\n`bb ` command agents run through bash, with a shared 1 MiB combined\nstdout/stderr ceiling and atomic structured over-limit errors); bb.agents.registerTool\n(static native tools with zod or JSON-schema parameters) and\nbb.agents.configure (one synchronous per-resolution callback selecting this\nplugin's own tool/skill ids and optional dynamic instructions; tools apply on\nthe next provider session start/resume, while busy skill runtimes defer catalog\nchanges); bb.ui\nregisterMentionProvider (host-rendered UI — no\nfrontend bundle needed); bb.status.needsConfiguration (report\n\"unconfigured\" instead of crashing); bb.onDispose (LIFO cleanup on\nreload/disable/shutdown).\n\nFrontend entries register React slots (homepageSection, settingsSection,\nnavPanel, threadPanelAction, fileOpener, messageDirective) and composer\ncustomizations via `app.composer.customize({ actions, plusMenu, banners,\nrichText })`; action/banner components use `useComposer()` and\n`useComposerView()`, while the host renders plus-menu rows and editor\ndecorations. The deprecated pre-1.0 `slots.composerAccessory` footer API was\nremoved; migrate controls to actions or the plus menu and larger content to\nbanners. Register all frontend surfaces via\ndefinePluginApp, use the hooks\nlisted above, and render vendored components; styling is Tailwind against\nthe host theme's tokens only (semantic classes like bg-background and\ntw-animate-css utilities compile in plugin builds).\n\nFor the complete authoring reference — exact signatures, working snippets\nfor every surface, the reload lifecycle, testing tips, and gotchas — use\nthe built-in `bb-plugin-authoring` skill (agents: it loads on demand;\nhumans: apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/\nin a checkout). The builtin `inline-vis` plugin renders\n`::inline-vis{file=\"demo.html\" height=\"480\"}` through the sidebar's\npath-shaped, sandboxed worktree HTML iframe preview; `height` is optional.\nIts card header includes an open-in-sidebar action for the source HTML file.\nThe `plugins/` directory contains every bundled plugin: the auto-installed\nbuiltins and the store-only BB Official GitHub, Docs, Memory, and Tasks\nplugins. The `examples/plugins/` reference plugins cover slack-bot (webhook\nbot), agent-enrichment (agent surfaces), composer-customization (all composer\nregions), and t3sidebar (a replacement sidebar thread list).", "fileName": "bb-guide-plugins.md", "kind": "instruction", "title": "bb Guide — Plugins", diff --git a/packages/templates/src/templates/bb-guide-plugins.md b/packages/templates/src/templates/bb-guide-plugins.md index 839d6063d2..ddd9add6fb 100644 --- a/packages/templates/src/templates/bb-guide-plugins.md +++ b/packages/templates/src/templates/bb-guide-plugins.md @@ -138,8 +138,7 @@ added/updated/unchanged counts. bb plugin search Search BB's official plugins (bundled with the app) bb plugin install Install a bundled official plugin by name - (github, docs, memory, tasks, - t3sidebar), a local + (github, docs, memory, tasks), a local path, builtin:, git:@, or npm:[@] @@ -195,14 +194,13 @@ added/updated/unchanged counts. BB Official plugins -BB's official plugins — GitHub, Docs, Memory, Tasks, and T3 Sidebar — ship -bundled inside the app itself. They appear in Extensions → Plugins → Browse +BB's official plugins — GitHub, Docs, Memory, and Tasks — ship bundled inside +the app itself. They appear in Extensions → Plugins → Browse and install with one click from the local bundled copy: no network, no download, no separate release. Install from the CLI by bare name -(`bb plugin install github`, `bb plugin install docs`, -`bb plugin install memory`, `bb plugin install tasks`, or -`bb plugin install t3sidebar`). Installed official plugins are pinned to the -bundled copy and update automatically when the BB app updates. +(`bb plugin install github`, `bb plugin install docs`, `bb plugin install +memory`, or `bb plugin install tasks`). Installed official plugins are pinned +to the bundled copy and update automatically when the BB app updates. For direct git:/npm: installs, updates are manual: `bb plugin outdated` checks tracking sources and `bb plugin update` applies compatible candidates. @@ -427,7 +425,7 @@ in a checkout). The builtin `inline-vis` plugin renders path-shaped, sandboxed worktree HTML iframe preview; `height` is optional. Its card header includes an open-in-sidebar action for the source HTML file. The `plugins/` directory contains every bundled plugin: the auto-installed -builtins and the store-only BB Official GitHub, Docs, Memory, Tasks, and T3 -Sidebar plugins. The `examples/plugins/` reference plugins -cover slack-bot (webhook bot), agent-enrichment (agent surfaces), and -composer-customization (all composer regions). +builtins and the store-only BB Official GitHub, Docs, Memory, and Tasks +plugins. The `examples/plugins/` reference plugins cover slack-bot (webhook +bot), agent-enrichment (agent surfaces), composer-customization (all composer +regions), and t3sidebar (a replacement sidebar thread list). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1347d938b7..bb35d8073b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1046,6 +1046,67 @@ importers: specifier: ^4.1.1 version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) + examples/plugins/t3sidebar: + dependencies: + '@hugeicons/core-free-icons': + specifier: ^4.1.3 + version: 4.1.3 + '@hugeicons/react': + specifier: ^1.1.6 + version: 1.1.6(react@19.2.4) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@bb/plugin-sdk': + specifier: workspace:* + version: link:../../../packages/plugin-sdk + '@radix-ui/react-context-menu': + specifier: ^2.3.3 + version: 2.3.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': + specifier: ^2.3.3 + version: 2.3.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/better-sqlite3': + specifier: ^7.6.12 + version: 7.6.13 + '@types/node': + specifier: ^22.0.0 + version: 22.19.10 + '@types/react': + specifier: ^19.0.0 + version: 19.2.13 + better-sqlite3: + specifier: 12.10.0 + version: 12.10.0 + jsdom: + specifier: ^29.0.1 + version: 29.0.1(@noble/hashes@2.0.1) + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' + typescript-7: + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 + vitest: + specifier: ^4.1.1 + version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) + examples/plugins/thread-chat-demo: devDependencies: '@bb/plugin-sdk': @@ -2766,55 +2827,6 @@ importers: specifier: ^4.1.1 version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) - plugins/t3sidebar: - dependencies: - '@bb/shared-ui': - specifier: workspace:* - version: link:../../packages/shared-ui - zod: - specifier: 4.3.6 - version: 4.3.6 - devDependencies: - '@bb/plugin-sdk': - specifier: workspace:* - version: link:../../packages/plugin-sdk - '@radix-ui/react-context-menu': - specifier: ^2.2.16 - version: 2.3.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@types/better-sqlite3': - specifier: ^7.6.12 - version: 7.6.13 - '@types/node': - specifier: ^22.0.0 - version: 22.19.10 - '@types/react': - specifier: ^19.0.0 - version: 19.2.13 - better-sqlite3: - specifier: 12.10.0 - version: 12.10.0 - jsdom: - specifier: ^29.0.1 - version: 29.0.1(@noble/hashes@2.0.1) - react: - specifier: ^19.0.0 - version: 19.2.4 - react-dom: - specifier: ^19.0.0 - version: 19.2.4(react@19.2.4) - typescript: - specifier: npm:@typescript/typescript6@^6.0.2 - version: '@typescript/typescript6@6.0.2' - typescript-7: - specifier: npm:typescript@^7.0.2 - version: typescript@7.0.2 - vitest: - specifier: ^4.1.1 - version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) - plugins/tasks: dependencies: '@bb/shared-ui':