Перейти к содержимому

Слоты фронтенда

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, а не ошибку загрузки модуля.

Билдер: шесть поверхностей регистрации

app-contract.ts
export type PluginAppSetup = (app: PluginAppBuilder) => void;
export function definePluginApp(setup: PluginAppSetup): PluginAppDefinition;
// PluginAppDefinition = { readonly __bbPluginApp: true; readonly setup: PluginAppSetup }
app.experimental_icons // ExperimentalAppIcons experimental
app.commands // PluginAppCommands stable
app.slots // PluginAppSlots — 22 methods stable
app.composer // PluginAppComposer stable
app.contentScripts // PluginAppContentScripts stable
app.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-]+)$/u
  • navPanel.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 и issuesstable
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() } | nullexperimental_
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 и --inkexperimental_
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(): куда попадают записи

app-contract.ts
scope · text · setText · updateText · clear · setTextEffect · setInputLock
addQuote · insertMention · experimental_removeMention · experimental_onSubmitted
focus · 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_Iconname · 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/.

Стилизация: скоупинг, заглушки, токены

packages/plugin-build/src/scope-plugin-utilities.ts
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.

Регионы билдера (5)

Регионы PluginAppBuilder: к ним обращаются через билдер, а не через app.slots.