Слоты фронтенда
bb 0.43.3SDK 0.4.104пин e865697 (desktop-v0.43.3)
Здесь описан frontend API: билдер и шесть его поверхностей регистрации, 22 слота, хуки, компоненты хоста для встраивания, стилизация и изоляция сбоев. @get-bb/plugin-sdk/app не поставляет собственный рантайм: он читает globalThis.__bbPluginRuntime.pluginSdkApp, а сборка заменяет импорт на заглушку (shim), ведущую к этому же глобальному объекту. Поэтому бандл плагина работает только внутри bb, и при импорте пакета вне хоста вы получите undefined, а не ошибку загрузки модуля.
Билдер: шесть поверхностей регистрации
export type PluginAppSetup = (app: PluginAppBuilder) => void;export function definePluginApp(setup: PluginAppSetup): PluginAppDefinition;// PluginAppDefinition = { readonly __bbPluginApp: true; readonly setup: PluginAppSetup }
app.experimental_icons // ExperimentalAppIcons experimentalapp.commands // PluginAppCommands stableapp.slots // PluginAppSlots — 22 methods stableapp.composer // PluginAppComposer stableapp.contentScripts // PluginAppContentScripts stableapp.experimental_sidebarFooter// ExperimentalSidebarFooter experimental
// app.hooks, app.settings and app.status do NOT exist: declarative settings// and plugin status live on the backend onlyПеред интерпретацией бандла хост проверяет бренд (brand). При каждой (ре)интерпретации он заново запускает setup с новым сборщиком (collector) и полностью заменяет набор регистраций плагина. Функция setup выполняется синхронно и проходит валидацию. Если валидация не проходит, хост сохраняет предыдущее поколение.
Проверки сборщика
- идентификаторы слотов, панелей, вкладок, команд и элементов подвала (footer-item):
/^[a-zA-Z0-9_-]+$/ messageDirective.id:/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/timelineRenderer.kind:/^(tool|[a-z0-9-]+\/[a-z0-9-]+)$/unavPanel.path: тот же паттерн, что у идентификатора («it becomes a URL segment»)fileOpener.extensions: каждое расширение —/^[a-z0-9]+$/, в нижнем регистре и без точки- сборщик отклоняет неизвестные поля регистрации; переименованные ключи nav-panel отклоняются с подсказкой нового имени
- идентификаторы уникальны внутри типа слота; команды делят один неймспейс между
app.commands.registerи устаревшимapp.slots.commandPaletteAction layoutпринимает только"padded"или"flush";run,isAvailable,mountиonActivateдолжны быть функциями
Собранные данные принимают форму CollectedPluginAppRegistrations — это те же 26 корзин (buckets), которые тестовая обвязка возвращает как CapturedPluginApp.
Хуки
| Хук | Возвращает | Стабильность |
|---|---|---|
useRpc<Contract>() | { call(method, ...args) }; реджектится с Error, где лежат сообщение сервера, стабильный code и issues | stable |
useRealtime(channel, handler) | void; активен, пока компонент смонтирован | stable |
useRealtimeConnectionState() | "connecting" | "connected" | "reconnecting" | stable |
useSettings() | { values: Record<string, string|number|boolean> | undefined; isLoading } (только несекретные) | stable |
useBbContext() | { projectId: string | null; threadId: string | null } — это весь контекст маршрута, треда и проекта | stable |
useBbNavigate() | toThread · toProject · toPluginPanel · toCompose · openThreadPanel · openUrl · experimental_openFilePreview · experimental_openFileExternally; каждый boolean означает «хост принял запрос», а не «ввод-вывод завершён» | stable |
experimental_useAppPanel() | { openFixedTab<Target>(options): boolean } | experimental_ |
experimental_useFixedTabTarget(tab) | { sequence, target, clear() } | null | experimental_ |
useComposer() | смотрите ниже: запись черновика, блокировка, упоминания, отправка, выбор | stable |
useComposerView() | { scope, layout: "expanded"|"compact"|"zen", draft:{text,isEmpty,attachmentCount}, run:{isRunning,isSubmitting} } | stable |
experimental_useSidebarThreads() | { status, threads, projects }; читает кэш хоста и подписки реального времени без дополнительных сетевых обращений; список не ограничен по длине, идентичность объектов стабильна, поэтому виртуализируйте строки | experimental_ |
experimental_useSidebarThreadActions() | open · openNewThread · setPinned · setRead · rename (без диалога) · archive (вместе с дочерними) · requestDelete (открывает окно подтверждения bb; удаление без подтверждения намеренно отсутствует) | experimental_ |
experimental_useSidebarThreadPullRequest(threadId) | { isLoading, pullRequest }; запрашивайте для каждой строки отдельно (opt-in), так как хук обращается к git-хосту | experimental_ |
experimental_useSidebarThreadSplit(threadId) | { splitProps, isAvailable, layout } | experimental_ |
experimental_useProviders() | { status, providers } — закэшированный список с хоста; благодаря ему плагину не нужно поставлять имена и иконки провайдеров | experimental_ |
experimental_useCodeTheme() | { mode, name, theme } — настоящий документ темы VS Code. Используйте его, только если рендерите код собственным движком (Monaco, CodeMirror). Не пытайтесь вычислить палитру на основе --canvas и --ink | experimental_ |
experimental_useBranches(args) | { branches, remoteBranches, isLoading, refresh() } | experimental_ |
experimental_useCheckoutState(args) | { isGit, unborn, detached, dirty, currentBranch, operation } | experimental_ |
Хуков useTheme, useThread, useProject и useRoute нет. PluginSidebarThreadIndicator — это уже разрешённый тип статуса ("unread-error" | "waiting-for-input" | "working-draft" | "workflow" | "background-agent" | "background-command" | "plan-mode" | "goal" | "runtime" | "draft" | "unread-success" | "none"). SDK не даёт готового компонента статуса: рисуйте собственный глиф и обрабатывайте незнакомые значения как "none", так как со временем bb добавляет новые типы.
useComposer(): куда попадают записи
scope · text · setText · updateText · clear · setTextEffect · setInputLockaddQuote · insertMention · experimental_removeMention · experimental_onSubmittedfocus · experimental_submit · experimental_setSelection
// where a write goes: into the editor of a queued message while it is being edited;// into the visible side-chat draft inside side-chat; otherwise into the thread's draft// inside a thread context; anywhere else (a nav panel, a homepage section) it seeds the// new-thread draft, which survives until the user sends or clears it.// setTextEffect and setInputLock are scoped to the CALLING plugin and lift themselves// when the slot unmounts or the composer's scope changes.experimental_submit отправляет сообщение через пайплайн композера хоста. Вложения, @-упоминания и (в новом треде) выбор провайдера, модели, уровня размышлений, уровня сервиса, режима разрешений и окружения уходят вместе с ним: плагин не может собрать этот кортеж самостоятельно. Метод возвращает отказ, если у области (scope) нет пайплайна (например, в редакторе очереди или боковом чате), если черновик пуст или если композер не готов. Метод experimental_setSelection устанавливает значения пикеров так, будто их выбрали вручную. Он резолвится, когда выбор применяется; ожидание загрузки каталога ограничено 15 секундами.
Компоненты хоста, которые можно встроить
| Экспорт | Пропсы | Стабильность |
|---|---|---|
ThreadChat | { threadId, variant?: "full"|"compact"|"timeline", layout?: "contained"|"document", focusRequest?, permissionPolicy?: "inherit"|"editable", className?, leadingContent?, messageActions? } | stable |
Markdown | { content, className?, experimental_document?: { threadId, rootPath, target } } | stable |
UrlLink | { href } и пропсы ссылки | stable |
experimental_FileLink | { target: ExperimentalLiveFileTarget, location?: ExperimentalFileLocation | null } | experimental_ |
experimental_NewThreadComposer | { onSubmit, default*…, initialPrompt?, placeholder?, layout?, focusRequest?, draftKey? } | experimental_ |
experimental_ProviderModelPicker | { value: { providerId, model, reasoningLevel, serviceTier? }, onChange, routing?, allowProviderChange?, align?, disabled? } | experimental_ |
experimental_PermissionModePicker | { providerId, value, onChange, routing?, align? } | experimental_ |
experimental_BranchPicker | { hostId, projectId, value, onChange, label?, placeholder?, disabled? } | experimental_ |
experimental_SourceCode | { content, path, overflow?, highlightedLines?, className? } | experimental_ |
experimental_Diff | { patch, path, view?, overflow?, showLineNumbers?, experimental_fullFileContents? } | experimental_ |
experimental_Icon | name · fallback? (по умолчанию Zap) · className? · style? · aria-* | experimental_ |
experimental_ProviderIcon | { providerKind, provider: { id, logoUrl?, icon?, strings? }, fallback? (Code), className? } | experimental_ |
- Алиасы в JSX для компонентов
experimental_*обязательны: JSX считает имя со строчной буквы встроенным элементом, поэтому<experimental_Diff />не скомпилируется. Задавайте алиасы прямо при импорте:import { experimental_Diff as Diff, experimental_FileLink as FileLink } from "@get-bb/plugin-sdk/app". ThreadChat— намеренное исключение из правила «SDK не предоставляет UI-кит». Хост берёт на себя загрузку таймлайна, стриминг, черновики, отправку, очередь, управление ходом (steer), остановку, вложения, элементы управления выполнением, ожидающие взаимодействия и отметки о прочтении. Не проксируйте данные треда через свой RPC. НастройкаpermissionPolicy: "inherit"(по умолчанию) привязывает отправки к дефолтным разрешениям треда и отключает пикер, поэтому поверхность плагина не может расширить права доступа.experimental_Diffберёт общий пул воркеров подсветки синтаксиса из контекста React. У панелей тредов и навигации этот пул есть, а у разделов главной страницы и настроек — нет. Поэтому там код рендерится без подсветки, но не ломается.experimental_NewThreadComposerдаёт пользователю выбор, но ваш плагин сам создаёт тред. После разрешенияonSubmitчерновик очищается, а при ошибке — сохраняется. Пропсыdefault*работают как начальные значения (seeds) и сравниваются по значению при каждом рендере: изменение любого из них после монтирования перезапишет весь выбор, включая ручные правки пользователя.- Удалено: старых
EmptyState,PageBodyиSpinnerбольше нет. Пишите собственные; эталонные реализации лежат вplugins/github/components/.
Стилизация: скоупинг, заглушки, токены
export function pluginScopeRoots(pluginId: string): string { return `[data-bb-plugin="${pluginId}"], [data-bb-plugin-root]:not([data-bb-plugin])`;}// the effective selector:// :where([data-bb-plugin=<id>],[data-bb-plugin-root]:not([data-bb-plugin]))// :where() keeps the scoping from adding specificity.// The host wrapper sets the attributes: PluginSlotMount.tsx renders// <div data-bb-plugin-root="" data-bb-plugin={pluginId} className="contents">- Сборка переписывает только блок
@layer utilities. Каждое правило получает две ветки: потомка${scope} ${selector}и составную${scope}${selector}. Если использовать+или~, остаётся только ветка потомка. Правило класса вне слоя утилит вызовет ошибку сборки, так как оно просочится на страницу хоста. - Правило
@scopeнамеренно не используется из-за высоких затрат на пересчёт стилей в WebKit; правило репозитория вAGENTS.mdссылается на этот файл напрямую. - Собственный
app.cssплагина приклеивается после стилей со скоупингом и не изолируется. - Время жизни таблицы стилей: элемент
<link rel="stylesheet" data-bb-plugin-css="<id>">живёт вdocument.headсо счётчиком ссылок и задержкой удаления в 1500 мс. ХукusePluginCssвыполняется внутриuseInsertionEffect. Это не глобальный CSS-хук приложения: для глобальных палитр используйтеbb.themesв манифесте. - Цвета: используйте только классы токенов Tailwind с хоста (
bg-card,text-foreground,text-muted-foreground,border-border,text-destructive). Не добавляйте кастомные цвета через@theme, литералы серого или написанные вручнуюoklch(...): проход Tailwind при сборке генерирует утилиты только для дефолтной темы, а жёстко заданные цвета ломают кастомные палитры. Каждый режим определяет две опорные точки:--canvasи--ink, а остальные нейтральные цвета выводятся черезcolor-mix. Вам не нужно читать их напрямую. - Вендоринг компонентов идёт через стандартный shadcn, работающий с реестром bb:
npx shadcn add @bb/select @bb/table. Командыbb plugin vendorне существует. Единственное отличие от стандартного shadcn: на компактных экранахDialogрендерится как нижняя шторка, сохраняя тот же API. - Сборка перенаправляет импорт
import { toast } from "sonner"(shimmed) на Toaster хоста. Никогда не монтируйте собственный<Toaster>. Для рендера кода и диффов беритеexperimental_SourceCodeиexperimental_Diff, а не библиотеку@pierre/diffsнапрямую: иначе вам придётся самостоятельно нормализовывать патчи и управлять темой кода, а также вы выпадете из механизма подмены рендерера.
Изоляция сбоев
| Сбой | Что видит пользователь |
|---|---|
| исключение при рендере аддитивного слота | плашка сбоя «plugin <id> crashed» или ваш собственный crashFallback |
| исключение в замещающем слоте | возврат к родному компоненту bb через crashFallback={<PluginOwnerRenderer />} |
| исключение в списке тредов или боковой навигации | интерфейс bb плюс тост |
messageDirective, бросивший исключение | исходный текст директивы |
| бандл не загрузился | status: "failed", закоммиченное поколение деактивируется, регистрации и CSS отзываются |
setup бросил ошибку | предыдущее поколение деактивируется, регистрации и CSS не публикуются |
| content script упал или превысил 10-секундный лимит | кандидат отменяется, уже смонтированные скрипты демонтируются в обратном порядке, ничего не публикуется |
Граница ошибок использует ключ ${pluginId}/${slotKind}/${slotId}[/${instanceId}]. Ключ сбоя попадает в множество crashedSlotInstances на уровне модуля. Слот остаётся мёртвым до конца сессии или пока resetCrashedPluginSlots(pluginId) не очистит его при перезагрузке. Диагностика публикуется в виде lastFailure.phase: "load" | "setup" | "mount" | "dispose". Сбой одного плагина не останавливает активацию другого. Вместо падения каждый замещающий слот получает пропс Original, привязанный к текущему вызову. Благодаря этому слот может делегировать рендер, не запуская заново механизм подмены. Алиас experimental_Original устарел и удалён в bb 0.42.
Слоты: по одной карточке на каждый
Типы слотов: аддитивные (рендерятся рядом с другими плагинами и родным интерфейсом bb), замещающие (подменяют компонент bb и откатываются к нему при сбое), эксклюзивные (допускают только один плагин на всю область) и host chrome (плагин не поставляет компонент, его рисует bb).
Методы-слоты (22)
Члены interface PluginAppSlots.
commandPaletteActionстабильный устаревшийapp.slots.commandPaletteAction({ … })experimental_appOverlayэкспериментальныйapp.slots.experimental_appOverlay({ … })experimental_browserToolbarActionэкспериментальныйapp.slots.experimental_browserToolbarAction({ … })experimental_diffRendererэкспериментальныйapp.slots.experimental_diffRenderer({ … })experimental_environmentProviderInputsэкспериментальныйapp.slots.experimental_environmentProviderInputs({ … })experimental_machineProviderInputsэкспериментальныйapp.slots.experimental_machineProviderInputs({ … })experimental_newThreadPanelActionэкспериментальныйapp.slots.experimental_newThreadPanelAction({ … })experimental_providerIconэкспериментальныйapp.slots.experimental_providerIcon({ … })experimental_sidebarNavigationэкспериментальныйapp.slots.experimental_sidebarNavigation({ … })experimental_sourceCodeRendererэкспериментальныйapp.slots.experimental_sourceCodeRenderer({ … })experimental_threadHeaderActionэкспериментальныйapp.slots.experimental_threadHeaderAction({ … })experimental_threadListэкспериментальныйapp.slots.experimental_threadList({ … })experimental_timelineRendererэкспериментальныйapp.slots.experimental_timelineRenderer({ … })fileOpenerстабильныйapp.slots.fileOpener({ … })homepageSectionстабильныйapp.slots.homepageSection({ … })messageActionстабильныйapp.slots.messageAction({ … })messageDirectiveстабильныйapp.slots.messageDirective({ … })navPanelстабильныйapp.slots.navPanel({ … })pendingInteractionстабильныйapp.slots.pendingInteraction({ … })settingsSectionстабильныйapp.slots.settingsSection({ … })sidebarFooterActionстабильныйapp.slots.sidebarFooterAction({ … })threadPanelActionстабильныйapp.slots.threadPanelAction({ … })
Регионы билдера (5)
Регионы PluginAppBuilder: к ним обращаются через билдер, а не через app.slots.
commandsстабильныйapp.commandscomposerстабильныйapp.composercontentScriptsстабильныйapp.contentScriptsexperimental_iconsэкспериментальныйapp.experimental_iconsexperimental_sidebarFooterэкспериментальныйapp.experimental_sidebarFooter