Every public plugin API member ships with an experimental_ prefix and an
entry here (see AGENTS.md, "CLI And Plugin API"). Each entry
summarizes the API, names where its behavior is documented, and lists what
must be settled before the prefix drops. Stabilizing means auditing the entry,
renaming project-wide, and deleting the entry in the same change. An entry
that stays experimental after an audit says so with the date and the open
question.
"Docs:" names a file in
plugins/bb-guide/skills/bb-plugin-authoring/references/, or
provider-plugin-api.md /
provider-bridge-protocol.md. An entry without
a "Docs:" line has no owning doc, so it keeps the essential contract itself.
ComposerCustomization.experimental_popups registers { id, label, component }
popups; PluginComposerApi.experimental_openPopup(id) opens one in that
composer and experimental_closePopup() closes it, both returning false when
they cannot act.
Docs: frontend-hooks-and-ui.md.
Before stabilization, verify multiple composers, selection retention, plugin reload and crash recovery, scope suppression, and compact Safari keyboard/drawer behavior. Verify multiple popups in one customization and popup-id uniqueness across customizations.
PluginAppComposer.experimental_registerCommand({ id, title, defaultShortcut?, run })
registers a palette and keyboard command that runs against the owning
composer's PluginComposerApi, rebindable under
plugin:<plugin-id>/<command-id> and sharing the app.commands id namespace.
Docs: frontend-hooks-and-ui.md.
Before stabilization, verify split panes, side chat, inline queued-message editing, palette invocation from each, plugin reload, and rebinding. Audit whether composer scopes should filter commands the way they filter other composer contributions, and whether commands need an availability callback.
experimental_page: "mobile" mounts a plugin settings section only on
Settings → Mobile, and only when that plugin owns the selected access
provider. Omitting it keeps the section on the plugin's configuration page.
Stabilization requires verifying placement isolation, plugin disable/uninstall, loading and failure states, and pairing lifecycle on Mobile.
Unprefixed by explicit request. Registers frontend commands
({ id, title, defaultShortcut?, isAvailable?, run }) for the quick palette
and keyboard bindings; the deprecated app.slots.commandPaletteAction alias
shares its validation and per-plugin id namespace.
Docs: frontend-renderer-slots.md.
Audit command identity, availability outside the palette, shortcut conflicts, and saved binding lifecycle before stabilizing the keyboard shortcut contract. Audit cross-platform conflicts and plugin lifecycle before extending the context model or default binding policy.
bb.rpc.register accepts experimental_discoverable and
experimental_description options, and method definitions accept
experimental_description. A discoverable method publishes its wire schemas
(Standard JSON Schema) and description. Discovery does not change RPC
authorization or dispatch.
bb.sdk.plugins.experimental_discoverRpc({ pluginId?, method?, signal? })
lists the published methods of loaded plugins. Methods disappear on unload, so
callers handle the race between discovery and invocation. The fake host
exposes experimental_publishedRpcMethods.
Before stabilization, audit schema export fidelity (especially refinements and
transforms), descriptor size and reference limits, lifecycle races, and
cross-plugin copied-schema compatibility. Verify bb plugin rpc list|inspect
is sufficient to implement a consumer without a shared contract package.
Method names carry optional versions; there is no negotiation.
bb.sdk.plugins.experimental_getSafeMode() returns { enabled }.
experimental_setSafeMode({ enabled }) returns { enabled, problems }, where
problems names each plugin that did not start when safe mode ended. While
safe mode is on, only plugins included with bb run: builtin provenance, or
an auto-installed bundled source. Every other plugin stays unloaded with
status disabled. Each plugin's own enabled flag is untouched. Enable keeps
an affected plugin unloaded, reload reports a failure, and install and update
are refused. The flag persists on the server and backs
bb plugin safe-mode [on|off].
Before stabilization, audit whether official store plugins should count as
included, whether a plugin calling experimental_setSafeMode should be
allowed to stop itself and others, whether the toggle should run
asynchronously for installs with many slow plugins, and whether startup needs
an out-of-band override (env var or flag) for a plugin that breaks the server
before the toggle is reachable.
bb.sdk.threads.queuedMessages.experimental_holdForEdit({ threadId, queuedMessageId })
(POST /threads/:id/queued-messages/:queuedMessageId/edit-hold) holds a
queued row while someone edits it and returns { leaseMs }. While the hold
is live, no automatic dispatch (idle drain, schedule, plugin recheck, host
reconnect, failed-retry) claims the row, and the idle drain also keeps the
drainable rows behind it queued; rows waiting on a schedule or plugin are not
held back, and explicit send-now still claims it. Calling again renews the
lease; an unrenewed hold lapses after leaseMs and the periodic sweep
dispatches the row. It answers 409 once a dispatch claimed the row and 404
for a missing row. Holding never changes updatedAt, so a pre-hold
expectedUpdatedAt stays valid; a successful queuedMessages.update clears
the hold. experimental_releaseEditHold (DELETE, same path) cancels
without saving and is a no-op for an unheld row; releasing or saving
re-attempts dispatch immediately. The app's inline editor holds on open,
renews every quarter lease, and releases on close. Any client may renew or
release a hold.
Before stabilization, audit whether holds need an owner token so two clients
editing the same row cannot release each other's hold, whether queue DTOs and
bb thread queue list should show that a row is being edited, and whether
the lease length suits throttled background tabs.
bb.sdk.plugins.experimental_pruneCache({ dryRun? })
(POST /plugins/cache/prune) deletes cached git and npm plugin versions
under <data>/plugins/cache that no installed plugin uses and returns
{ dryRun, removed, bytes }; each removed entry has pluginId (null for an
unrecorded directory), version, path, and bytes, counting a shared
monorepo checkout once. It removes every non-active version regardless of
age and keeps active versions, versions a pending or restoring rollback
needs, directories containing or inside an installed plugin's root, and
in-flight staging directories. It runs under the plugin registration lock.
Removing a plugin deletes its cached versions at once, and a successful update
deletes replaced versions once activation stabilizes (its state snapshot
stays for the 7-day retention window). It backs bb plugin prune [--dry-run]
and the palette's Clean up plugin cache.
Before stabilization, audit whether the response should report
retained-but-unused sizes so a UI can show reclaimable space without a dry
run, and whether stale .staging, .promoting, and .corrupt directories
left by crashes should be collected too.
Every bb.rpc.register handler receives a context whose experimental_caller
is { kind: "plugin", pluginId } for a loaded plugin's
bb.sdk.plugins.callRpc and { kind: "client" } for everyone else. A
per-load token makes the identity unforgeable by clients.
Docs: backend-events.md, backend-sdk.md.
Before stabilizing, audit whether other surfaces (HTTP routes, agent tools,
CLI commands) need the same identity, whether a plugin needs to know the
calling app window or thread, and whether in-process plugins that read other
plugins' memory make the token a meaningful boundary for third-party plugins.
bb account relies on it to keep /api/connect/ requests, credential reads,
refused-credential reports, and credential adoption to the connect plugin.
Registers an exact-path WebSocket upgrade in the plugin's
/api/v1/plugins/<id>/http/ namespace. It shares HTTP route auth modes.
Sockets from a replaced or disabled plugin generation close with code 1012.
The test harness drives it with experimental_openWebSocket.
Docs: backend-events.md, testing.md.
Audit before stabilizing.
- Confirm the HTTP namespace and exact-path rule cover plugin proxy use cases without a separate WebSocket namespace or parameterized routing.
- Decide whether subprotocol negotiation belongs in the context or returned handler contract.
- Confirm string and
Uint8Arrayare sufficient for frame data and sends. - Confirm code 1012 is the right reload/disable signal and whether plugins need a distinct disposal reason.
- Confirm the callback error policy should keep the socket open after an isolated message-handler failure.
Registers one host-scoped readiness resolver beside a provider environment
contribution. When the bridge reports unauthenticated or expired, a
non-null answer presents the provider as ready with its label and status
message.
Docs: providers.md.
Audit before stabilizing.
- Confirm readiness should override only
unauthenticatedandexpired, not installation or unknown failures. - Decide whether the contribution should report account identity or usage in addition to a label and status message.
- Confirm the first available resolver in plugin load order is the right arbitration when several credential proxies target one provider.
- Confirm a five-second timeout is appropriate for host-scoped credential availability checks.
Registers one resolver per provider per plugin that returns up to 32
environment entries for each session and turn, and for model catalog listing
with a null thread and project. Each entry is a literal or a
server-relative path the host expands. Conflicts resolve in plugin load
order, and the values override the shell environment.
Docs: providers.md.
Audit before stabilizing.
- Confirm one resolver per provider is sufficient when plugins need multiple independently disposable features.
- Confirm plugin load order is the right deterministic conflict policy.
- Confirm the 32-entry cap and five-second timeout fit providers that resolve short-lived credentials.
- Decide whether
serverPathneeds structured query parameters or non-HTTP server endpoints before accepting more shapes. - Confirm
reasonshould remain required and whether event consumers need a stable machine-readable purpose beside it. - Decide whether the context and entry types should stabilize with the method or remain experimental for a longer compatibility window.
Open windows left by the SDK 0.4.16 stabilization renames. The bb-app CHANGELOG records the renames themselves.
- Renamed declaration, tool and navPanel fields (
experimental_strings,experimental_presentation,experimental_fixedTabs, …) are rejected at registration with a message naming the new field. - Presentation-less
toolCallrows pass through the legacy-data adapter (upgradeLegacyToolItemin@bb/domain). It renders Read/Grep/Glob and read/grep/find/ls asfileRead/searchrows and suppresses Task*/Todo*/ToolSearch bookkeeping calls. The pi bridge's live generictoolrows still take this path. To close the window, pi stampsfileRead/searchpresentation, thelegacy-tool-item-backfillmigration stamps old rows, andpresentationbecomes required onitem.open/item.close. Then the adapter and its tests are deleted.
Unprefixed exports of @get-bb/plugin-sdk/provider-bridge with no consumer in
this repository. They stay because a third-party bridge compiled against an
SDK before 0.4.16 may import them, and dropping a published name is a
breaking change. Remove them at the next major version. The
provider-bridge-scheduled-removals SDK test holds the facade to this list.
Re-exported from @bb/domain, where each still has core consumers:
acpNativeReasoningSchemaacpPermissionCliSchemaacpReasoningCliSchemaextensionKindSchemainteractionRequestPayloadSchemaisExtensionKindisUserQuestionPendingInteractionPayloadisUserQuestionPendingInteractionResolutionproviderRecoveryKindValuesthreadEventItemPresentationSchemathreadEventSearchModeSchema
Aliases of moved definitions. The ACP pair aliases the ACP kit's
experimental_acpLaunchSpecSchema / AcpLaunchSpec and normalizer
(@get-bb/plugin-sdk/provider-bridge/acp). The task-tool pair has no
replacement.
hostDaemonAcpLaunchSpecSchemanormalizeHostDaemonAcpLaunchSpecclaudeTaskToolNameSchemaclaudeTaskToolOutputSchemaHostDaemonAcpLaunchSpecClaudeTaskToolOutput
Also kept only until the next major:
- On
@get-bb/plugin-sdk/provider-bridge/acp,AcpAgentProfile, a deprecated alias ofAcpLaunchSpec. - On
@get-bb/plugin-sdk/provider-bridge/testing,CONFORMANCE_ASSEMBLED_EVENT_METHOD. The conformance kit assemblesthread/deltaitself and reads nothing under that method. - The
disallowedToolsfield ofthreadStartParamsSchema,threadResumeParamsSchemaandthreadForkParamsSchema. The runtime never populates it.
experimental_useSidebarThreadActions and PluginSidebarThreadActions
(@get-bb/plugin-sdk/app), with the testing harness's SidebarActionCall /
inspection.sidebarActionCalls, are superseded by the thread action
registry, useSdk().threads, and useBbNavigate().toThread / toCompose;
experimental_archiveEnvironmentThreads moved to
experimental_useArchiveEnvironmentThreads. Like useComposerView they are
@internal: published declarations drop them, while the runtime keeps
implementing them for installed plugins. Nothing in this repository calls
them.
PluginSettingDescriptor.experimental_schema is a synchronous,
non-transforming Standard Schema validator. The server runs it on every
proposed value. PluginSettingsHandle.experimental_set validates and persists
the plugin's own fields, like a settings route or bb plugin config write.
Docs: backend-foundation.md.
Audit before stabilizing.
- Confirm synchronous, non-transforming validation remains sufficient.
- Decide whether settings errors need structured issue paths in addition to the first user-facing message.
- Confirm schemas should continue running for defaults at registration.
- Exercise concurrent route, CLI, and plugin-owned writes, including secret
values and unsets, before stabilizing
experimental_set. - Decide whether schemas and server-side writes stabilize independently.
A numeric setting descriptor stores a finite number, renders a number
input, converts CLI values, and reads legacy numeric strings as numbers.
Docs: backend-foundation.md.
Audit before stabilizing.
- Decide whether common minimum, maximum, and step metadata belongs directly
on the descriptor instead of only in
experimental_schema. - Confirm clearing a number input should continue to unset the stored value.
- Decide how long legacy stored numeric strings should be coerced on read.
- Exercise decimal and exponent input across browser engines and the CLI.
on("message.dispatch", handler) answers core's admission checkpoint before
every message reaches a provider. A handler returns proceed, wait (queue
the message with a reason and optional sendAt), or reject (409).
Handlers run as a fail-closed chain in install order under one server-wide
lock. recheck("message.dispatch") schedules a re-attempt of every
plugin-queued row and resolves when the walk is scheduled.
Docs: backend-events.md.
Audit before stabilizing.
- Grouped dispatch authors.
initiator,senderThreadId,originandoriginPluginIdsummarize every row inqueuedMessages, and reportmixedwhen the rows disagree. Verify plugins handle mixed origins independently of authors. queuedMessageis emitted but untyped. Core still emits the first queued row or null under the old name for previously built handlers. Remove that field and its shim before stabilizing.startedOnBehalfOfis emitted but untyped. Core still sets it on the context for handlers built against an older SDK. Drop the context field and its shim before stabilizing. The queued row's internalrequested_by_initiator/requested_by_thread_idare unaffected.- One hook is not a shape. The registry, the map and the
on(hook, handler)signature are built for several hooks, and there is one. Confirm the second hook fits the shape before stabilizing it, or collapse the argument. waitreturns no row id. The id arrives later onmessage.queued, so a plugin that must act on its own wait correlates by ordering or re-queriesthreads.queue.list({ waitHolder }). Decide whether the decision should be able to name a correlation key.- A wait has no plugin-driven release, only a plugin-driven re-ask.
rechecknames no row, so one resolved condition wakes every plugin-queued row and every handler re-decides. Confirm the whole-queue walk is still right with many plugin waits and waiters, and decide whether a scoped variant is worth the correlation problem it reintroduces. recheckis unauthenticated in both directions. Any plugin can wake rows held by any other, and core does not tell a handler why it is being re-asked. Confirm no plugin needs to distinguish "core's clock" from "somebody asked".- Resolving on schedule is a contract. A caller cannot await the walk, so it cannot observe whether its own row went. Confirm resolving on completion, with its lock-reentrancy hazard, is genuinely unwanted rather than merely unbuilt.
- A handler's
sendAtand a user's--send-atare the same column. Confirm nothing renders a plugin's instant as a user's schedule. - Send-now bypasses every plugin check, including a content-policy
reject. Confirm it, or splitwaitbypass fromrejectbypass. - "Never started" is a thread status. Confirm
pendingis maintained everywhere the old event-log probe was consulted. - The context DTOs.
thread,project,environment,hostandqueuedMessageare public DTOs; confirm they are what a plugin should couple to. - The single server-wide lock. One slow handler delays every dispatch in the server, up to its 10 s box.
Fires after core commits a pending interaction. It carries the public thread and pending interaction DTOs and is observe-only.
Docs: backend-events.md.
Audit before stabilizing. Confirm that all plugins should receive provider and plugin interaction details. Confirm that the full interaction DTO remains the correct payload instead of an id that requires a fresh SDK read. Decide whether this event needs matching resolved, cancelled, or interrupted events.
These are observe-only announcements. message.queued (on first queue and on
every rewritten wait) and message.dispatched carry the ThreadQueuedMessage
DTO. message.cancelled fires when a queued row is deleted before dispatch.
turn.failed carries ids and failure facts after a thread lands in error.
Docs: backend-events.md.
Audit before stabilizing. These are the only non-thread events on
bb.events.on, so renaming PluginThreadEventPayloads and
PluginThreadEventHandler project-wide is part of stabilizing. Decide whether
every plugin should see every queued row (today it does, and filtering on
entry.waitingOn is the documented pattern) or only rows whose wait it owns.
Decide whether message.queued firing on every rewritten wait is what a
listener wants, or whether a separate message.updated belongs alongside it.
The timeline event already distinguishes the two. Rows that vanish with their
thread fire the thread event instead of message.cancelled. Confirm that
split is the teardown signal a plugin holding resources for a waiting message
needs. For turn.failed, confirm the payload answers every question a retry
policy asks without replaying the event log. Attempt caps are entirely the
plugin's; core enforces no ceiling beyond one live retry row per original
request.
Registers an environment provider: a named place threads run. Ids are flat
and first-wins, and the persisted owner plugin is the only one that can
recover or remove its resources. It declares requires eligibility facts,
Standard Schema inputs, optional validate, and durable create/remove
resource operations that core drives. recheck schedules another pending
launch attempt. The built-in checkout, worktree and personal workspace
behaviors are first-party plugins.
Docs: backend-events.md.
Audit before stabilizing.
- Requirement vocabulary.
requireshas four booleans, each a fact core evaluates before the thread exists; everything else goes throughinputs. Decide whether the next fact fits that shape, and whethergitCheckoutshould carry more than "a git repository with commits", before it is frozen. 1a. Inputs at the boundary. The schema is plugin code core runs inside the request: a slow or throwing schema costs the request, and the JSON Schema conversion happens once at registration and may fail load. Decide whether core should bound the parse, and whether the row should store the raw request value alongside the parsed one so a schema change can re-parse it. - Single-provider invocation. Confirm asking the one named provider (vs.
the
message.dispatchchain) stays the right model once several provider plugins ship, and that the "not registered" wait + backoff is the right degradation for an uninstalled plugin. - Undeclared facts:
suggestedBranchNameandprojectCheckout. Every create gets a branch name built from the thread title andmanagedBranchPrefix, and the project's checkout on its machine, whether or not the provider uses them. Decide whether both belong on the context for everyone or behind a declaration flag, and whether a path is all a provider needs to know about a checkout it did not make. - Durable operations. Audit cancellation races, provider reload during work, and restart checkpoints against the environment-provider contract below.
- Compatibility fields. Retain the deprecated
managed,isWorktree,workspaceProvisionTypeand display-kind fields for the agreed cycle.
Exports the resource-operation contract for
bb.experimental_environments.register. It also exports optional
availability(context) and restore(context). availability is cached per
project and machine, re-checked at thread creation, and reported to pickers
as machineAvailability. restore is the only way core rebuilds a destroyed
environment.
PluginEnvironmentProviderCreateContext.experimental_claimPath(path) durably
reserves a host path on the launch row before mutation, and resolves false for
competing claims or stale attempts. Values keep their prefix, and named types
are unprefixed.
Docs: backend-events.md.
Audit before stabilizing. Verify monotonic attempts and path-key recovery
across cancellation/restart; per-environment removal serialization; retry
caps and timing defaults (5 minutes/60 seconds/30 seconds/3/per-thread/null);
resource privacy and the 16 KiB boundary; create-timeout aborts; and the
read-only lifecycle projection. Decide whether restore should stay a separate
method or become a create mode. Decide whether a provider needs a way to
decline before the restore button is offered (git worktree fails after the
click when no branch was recorded). Decide when to stop passing the retired
rebuild: false / previous: null create fields at runtime. Audit
availability message ownership, create-time error presentation, the
interaction between the requires floor and provider decisions, and whether
the current decision timeout is appropriate. Stabilize experimental_claimPath
after auditing restart, cancellation, competing checkout, and
path-reservation behavior.
app.slots.experimental_environmentProviderInputs, experimental_BranchPicker, experimental_useBranches and experimental_useCheckoutState (@get-bb/plugin-sdk/app)
The slot registers { environmentProviderId, component }, the control the
New Thread environment picker renders beside that provider. Its props are
{ projectId, target, value, onChange }. target is
{ kind: "existing-host", hostId } or { kind: "new-host" }. The control
reports { status: "ready", value } or { status: "blocked", reason }.
Inputs are persisted and readable by every plugin, so they must hold no
secrets. The branch picker, branch hook and checkout-state hook are the host's
branch and checkout primitives for such controls.
Docs: frontend-components.md (environment row, branch and checkout
primitives). Only the API index names the slot registration itself.
Audit before stabilizing.
- Slot placement. The control renders where the branch picker sits. Confirm that stays right for providers with larger configuration (a dialog escape hatch?) before freezing.
- Validity channel. Confirm ready/blocked remains sufficient and decide whether the picker should run the published JSON Schema client-side before submit.
- Branch API breadth. Confirm the picker props and branch-hook result stay sufficient for providers, with branch loading, search and remote refresh owned by the host and selection models owned by the plugin.
- Missing slot. A provider whose schema requires non-empty input and
whose app entry registers no slot cannot be picked from the app (the CLI
can still send
--environment-inputs). Decide whether load should refuse that pairing. - Mount-time default. A control that wants the row submittable at once reports ready from an effect on mount. Decide whether the registration should instead declare a default value.
Registers { machineProviderId, component }, a compact control for one
machine provider's inputs. It renders in the New Thread toolbar when a
selected composition declares machine inputs. The component receives
{ value, onChange } and reports ready JSON or a blocked reason. The value
must contain no secrets.
Docs: frontend-components.md.
Audit before stabilizing. Confirm the compact-chip and responsive-drawer shape works across machine providers, that ready/blocked is sufficient, and that provider changes and crashed controls cannot retain stale launch inputs.
Registers a project-independent machine provider: required id, displayName,
description, icon, create, reconcileCleanup and remove; optional
ephemeral, Standard Schema inputs, availability, validate, and paired
suspend/resume. Core owns enrollment, durable launches, cleanup retries and
lifecycle transitions. Plugins own allocation, filesystem preservation and
idle policy. The lifecycle context's checkpoint(resource) persists a
bounded (16 KiB), credential-free recovery record.
Docs: backend-machines.md.
Before stabilization, verify registration validation, creation-key ownership, interrupted allocation cleanup, checkpoint rejection, resource privacy, removal serialization and same-identity restoration. Machine lifecycle uses removing; removal retry timing is internal, not a public retirement policy. The checkpoint requires real-DB crash recovery after allocation and before bootstrap, competing lifecycle operations, wrong-owner rejection and no duplicate enrollment. Standalone SDK submit/launch/follow/cancel must match CLI create/status/cancel.
Exports the machine provider's definition, input, availability/validation, create and lifecycle context, progress, and resource/removal types. These supporting declarations belong to the experimental machine namespace, so their unprefixed names do not mean they are stable.
Docs: backend-machines.md.
Stabilization follows bb.experimental_machines above.
A manifest map from names to plugin SVG files. Timeline presentation glyphs
and provider icon may reference an entry as "<pluginId>/<name>". The map
is validated at build and load. Ingest rejects a glyph that is not the
emitting plugin's own declared icon. bb.branding.icon refuses the namespaced
form.
Docs: quickstart.md (manifest and SVG rules), providers.md (provider
icons), backend-cli-agents.md (tool presentation).
Audit before stabilizing. Decide whether the key becomes icons. The
strict manifest schema makes that rename breaking, so the stabilization
release must accept both for one release. Decide whether bb.branding.icon
should resolve a self-referencing namespaced glyph through the map instead of
refusing it. Open question for a major release: whether to tighten logos
and provider icons at install and load, to the build rules or to the full
declared-icon rules. Today logos are checked only at build for script
vectors, and nosniff plus a default-src 'none' CSP keep served files
inert. Either tightening fails installed plugins whose artwork is an ordinary
tool export. It therefore needs an audit of installed marketplace and
third-party logos and a migration window first. Decide whether a full-colour
mode ({ path, mode } values) is wanted; today every icon is a monochrome
mask. Decide whether toolUse approval presentations are checked at ingest
too (today only timeline rows are). Settle whether a row persisted with a
namespaced glyph should ever be rewritten when the plugin renames or removes
the icon; today it simply falls back.
Kept experimental (2026-08-22). Drop the legacy input and settle the image policy, then stabilize.
Converts a decoded bb tool-call response into the ordered text and
inline-image content blocks that MCP and Pi tool results accept. It takes
ordered contentBlocks or the legacy aggregate { content, images }.
Audit before stabilizing. Confirm that MCP and Pi continue sharing this content-block vocabulary; decide whether legacy aggregate fields still need to be accepted; and define any image MIME validation, decoding, or payload-size policy at the server boundary.
Kept experimental (2026-08-22). Five members remain, each with an open question below. The kit grows with a consumer, not ahead of one.
experimental_acpProviderBridge is the generic Agent Client Protocol bridge a
plugin re-exports, backed by packages/provider-bridge-acp (the official
@agentclientprotocol/sdk transport, turns for agent-started work, slash
commands and session options as bb thread state, elicitation forms as
question cards, sign-in guidance from advertised auth methods). It launches
the agent named by providerOptions.acpLaunchSpec with the dialect named by
acpDialect in bridge options. experimental_probeAcpAgent asks an installed
agent for its agentCapabilities, and experimental_acpAgentProbeSchema
validates the answer across a host RPC boundary.
experimental_acpLaunchSpecSchema / AcpLaunchSpec is the launch spec the
bridge parses.
experimental_registerAcpDialect adds a dialect. A plugin calls it at module
load in its bb.host artifact with an AcpDialect and names the id as
acpDialect in its registration. Empty and built-in ids (acp, cursor,
grok, omp, opencode) throw, re-registering an id replaces it, and an
unregistered id resolves to the generic dialect. bb's own dialects resolve
through the same registry (resolveAcpDialect).
@get-bb/plugin-sdk/provider-bridge/acp-next, the subpath the kit was
previewed under (2026-10-09), re-exports this kit unchanged and is
deprecated. Each subpath is bundled with its own dialect registry, so a
plugin must import every member from one of them. Remove it at the next
major.
Docs: providers.md (ACP agents), provider-bridge-api-index.md. No doc
covers the probe or launch-spec contracts.
Audit before stabilizing. Decide what probeAcpAgent owes a caller.
Today it spawns the agent with a 10 s timeout, advertises the bridge's own
client capabilities, and answers -32601 to anything the agent asks. Settle
whether the timeout, the client capabilities and the refusal are the caller's
to choose. Decide whether AcpDialect is the right shape for a third-party
agent: its hooks are optional and unversioned, so a new one is a silent
capability change for every dialect. Decide whether a dialect should be named
by value in the provider registration instead of by id. Settle whether the
bridge and its dialect registry should be a factory rather than a module
singleton before a host artifact needs two configured differently. For
acpLaunchSpecSchema, the shape is stored in the ACP plugin's customAgents
setting and in registrations' bridge options. A change therefore migrates
stored agents; decide what a plugin is owed when the spec grows a field.
Kept experimental (2026-08-22). No third-party agent has validated the relative-path / 32-root rule or the per-root options, and the split from the per-workspace resolver is new.
Names the directories a provider's agent reads skills from: user relative
to the host home, and project relative to the workspace. bb lists those
skills beside its own.
Docs: providers.md, provider-plugin-api.md §1.
Decided (2026-08-22): there is no absolute side; host-specific roots go
through experimental_resolvesNativeRoots.
Audit before stabilizing. Decide whether the two-bucket shape (user,
project) is the right vocabulary or whether a root should name its own base
explicitly. Confirm the 32-root cap and the relative-path rule against a real
third-party agent. Decide whether this belongs on the declaration at all or
should be reported per host by the bridge, since where an agent keeps its
skills can differ per machine (today that difference is the resolver's job).
Audit whether the per-root options and symlink boundary rule are the right
vocabulary, or whether a root should carry a shape like the resolver's answer
does.
The same two-sided declaration, with the same per-root options, for flat
directories of *.md slash-command prompt files.
Docs: providers.md, provider-plugin-api.md §1.
Audit before stabilizing. Decide whether commands and skills should stay
two declarations or become one list of typed roots; confirm that a flat
*.md directory is the only command layout a third-party agent needs.
Declares that the plugin's bb.host entry serves
resolveNativeRoots({ providerId, cwd }). bb scans the answer beside the
declared roots (cached ten seconds). A failed answer yields no resolved roots
and never fails the listing.
Docs: providers.md, provider-plugin-api.md §1.
Audit before stabilizing. Decide whether the flag should exist at all or whether the server should detect the method on the host entry; and settle the cache TTL and the invalidation set against a real multi-host setup.
The one-method host RPC contract (resolveNativeRoots). Input is
{ providerId, cwd: string | null }. Output is { skills, commands }, each a
list of host-absolute roots with origin, recursive, ancestors,
namePrefix, and a shape that tells the daemon how to read the root.
Docs: providers.md.
Audit before stabilizing. Confirm the shape vocabulary covers a third-party agent's layouts; decide whether the contract should accept a relative path the daemon resolves (so a plugin need not know the host home) and whether the 256-root cap per side is right.
Checks a resolveNativeRoots answer root by root against the contract. It
drops each failing root with a warning, and truncates a side past 256 roots,
so one bad root cannot void the whole listing.
Docs: providers.md.
Audit before stabilizing. Decide whether the per-root leniency belongs in
the contract's output schema itself (drop at the boundary, report the drops
in the answer) so a plugin cannot forget to call the helper, and whether
dropped[].reason is a contract a resolver's test may pin or free text for
the log.
Vendor plugin roots (experimental_resolveClaudePluginRoots and experimental_resolveVendorPluginRoots, @get-bb/plugin-sdk/host)
experimental_resolveVendorPluginRoots({ plugins, layout }) walks vendor
plugin directories into native roots with namePrefix: "<plugin>:".
layout: "claude" reads each plugin's root SKILL.md, skills/, commands/,
then its manifest skills and commands entries. layout: "grok" reads only
the manifest entries, each recursive.
experimental_resolveClaudePluginRoots({ cwd, homeDir, env }) reads Claude
Code's installed-plugin registry and enablement settings and runs the walk.
Its answer also carries claudeDir. Two shared rules apply. User-origin
plugins follow skill symlinks, project-origin plugins do not, and command
components never follow links. Within an answer each path appears once per
side, and the first root to claim it wins.
Audit before stabilizing. Decide whether the walk should keep taking a
layout name (claude, grok) or the two facts behind it (conventional roots
on or off, directories recursive or flat) once a third vendor layout appears;
whether claudeDir belongs on the answer or the reader should also answer the
config directory's own roots; whether a skills-only caller should be able to
ask for one side; whether the repeated-path rule should be the contract's
(drop at the boundary, first root wins) rather than each helper's; and whether
a plugin's name should be validated as a name prefix here instead of at
experimental_filterResolvedNativeRoots.
A type: "string" descriptor flag that renders a multi-line monospace editor.
The stored string is unchanged. A descriptor that combines it with
secret: true is refused.
Docs: backend-foundation.md.
Audit before stabilizing.
- Name and shape. Decide whether to stabilize as a boolean
multilineor replace it with ajsondescriptor type that validates at the boundary (pretty-printing, parse errors shown in the form, a typed value fromsettings.get()), which would make this flag redundant for its first consumer. Amultilineboolean still has a use for plain lists, so the two may coexist. - Mobile. Confirm that phones need the editor at all (a JSON array is hard to type on a soft keyboard), or whether the mobile form should show the value read-only with an "edit on desktop" note.
- Unknown descriptor fields. The strict client schema turns every new presentation hint into a client release. Decide whether descriptor schemas should tolerate unknown fields so a hint degrades to the one-line input on an older client instead of failing the whole settings view.
POST /threads/plugin-metadata with { pluginId, threadIds } (1–200 ids)
returns { threads: { threadId, metadata }[] } for each thread holding that
plugin's namespace, archived and deleted threads included, in one indexed
query; threads without a namespace and corrupt namespaces are omitted. The
plugin-bound SDKs default pluginId to the caller. First caller:
push-notifications' Notifications thread action.
Docs: backend-sdk.md.
Audit before stabilizing.
- Route binding. Any caller can read any plugin's namespace, as with the single-thread metadata routes. Decide whether plugin requests should carry an identity the routes can enforce.
- Batch size. 200 ids per request; callers chunk. Revisit if the attributes layer replaces per-registration reads.
POST /threads/ancestors with { threadIds } (1–200 ids) returns
{ threads: { threadId, ancestorIds }[] }, parent first up to the root,
for every existing thread including archived and deleted ones, in one
recursive primary-key query. First caller: push-notifications, so a parent's
level follows the current tree.
Docs: backend-sdk.md.
Audit before stabilizing.
- Shape. Decide whether callers also need each ancestor's thread
(title, status) rather than only ids, and whether it should merge with
experimental_listDescendantsinto one tree read. - Batch size. 200 ids per request; callers chunk, like
experimental_listPluginMetadata.
POST /threads/descendants with { threadIds, includeArchived?, includeHidden? } (1–200 ids) returns { threads: { threadId, descendantIds }[] } in breadth-first order for every existing, non-deleted thread.
Archived and hidden descendants are omitted unless flagged, matching
threads.count, but the walk passes through them; anything reachable only
through a deleted thread is omitted. One recursive query over
threads_parent_idx. First caller: push-notifications, which republishes a
subtree's levels when a thread's level or parent changes and on
thread.unarchived.
Docs: backend-sdk.md.
Audit before stabilizing.
- Shape. Decide whether callers need the tree's edges (each
descendant's parent) or thread fields rather than a flat id list, and
whether it should merge with
experimental_listAncestors. - Size. A subtree of live threads has no cap, and the query walks archived threads even when it omits them. Decide whether to page.
- Omitted threads. Unlike
experimental_listAncestors, deleted threads are always omitted and archived and hidden ones by default. Confirm that callers never need deleted ones. No event announces a hidden thread becoming visible, so a caller that skips hidden threads cannot refresh one when it appears; decide whether visibility changes need an event.
A type: "select" descriptor record from option value to display label,
shown in the settings form. Stored values, defaults, settings.get() and
bb plugin config keep the option values, and labels for values outside
options are refused at define time. First consumer: push-notifications'
level settings.
Docs: backend-foundation.md.
Audit before stabilizing.
- Shape. Decide between this parallel record and
optionsaccepting{ value, label, description? }objects; the object form keeps labels beside their values and makes per-option descriptions possible, but every reader ofoptions(host policy, server contract, CLI, settings form) has to handle both forms. - CLI.
bb plugin config <id>still lists raw option values, which is whatsetaccepts; decide whether it should also print labels.
Kept experimental (2026-08-22). A bare data-directory path does not stabilize.
The server's data directory. Its only consumer is the Account Pool plugin,
which keeps secret files under plugins/<pluginId>/secrets/accounts.
Docs: backend-foundation.md.
Audit before stabilizing. Decide whether the Account Pool plugin's secret
files can move to a plugin-scoped primitive so the member can go. If it stays,
decide whether a bare path is the right shape or whether a plugin should get
named accessors for the bb-managed locations it may use. A path invites
writes into bb's directory, which bb.storage exists to prevent.
The operator-configured public app URL from BB_APP_URL, or null.
Docs: backend-foundation.md.
Audit before stabilizing. Decide whether BB_EXTERNAL_URL or the bb
connect URL should supply this value when BB_APP_URL is empty. Confirm that
one public URL has clear behavior when a server has several access paths.
Kept experimental (2026-08-22). The recording entry shape is a de-facto
fixture format consumed by the testing kit, so it must be frozen together
with experimental_readBridgeRecording / experimental_replayRecording. The
{ threadId | null } scope is untested against a multiplexing bridge.
experimental_recordProviderChildIo tees a provider child's stdio into record
mode and is a no-op when record mode is off.
experimental_isProviderBridgeRecording reports whether record mode is on.
Docs: provider-bridge-protocol.md "Record
mode", providers.md.
Audit before stabilizing. Decide whether the bridge kit should own the
spawn itself (one helper that spawns and records) instead of a post-spawn
hook; confirm the { threadId | null } scope is the right key once bridges
multiplex several threads over one child; and settle the recording entry
shape ({ ts, run, seq, dir, line }) as a documented fixture format.
Portable provider spawn (experimental_spawnPortableProcess, experimental_killPortableProcess) (@get-bb/plugin-sdk/provider-bridge)
experimental_spawnPortableProcess({ command, args, cwd?, env?, stdio?, detached? })
returns a ChildProcess launched the way the daemon launches one. On Windows
it resolves PATH/PATHEXT, runs npm .cmd shims, and hides the console;
elsewhere it behaves like spawn. experimental_killPortableProcess(child, signal)
terminates the whole process tree on Windows and sends signal to the child
elsewhere.
Audit before stabilizing. Decide whether bridges should receive the managed
process handle (an awaited stop that reports whether the tree is confirmed
gone) instead of a raw ChildProcess plus a fire-and-forget kill. Confirm
argument quoting for .cmd targets with untrusted arguments, and whether
detached belongs in the public shape.
Kept experimental (2026-08-22). It stabilizes together with
experimental_defineProviderBridge / experimental_apiVersion in the
bridge-kit audit.
A request handler throws it to reject the request with a JSON-RPC code and
error.data.recovery { kind, message, retryable }.
Docs: provider-bridge-protocol.md "Recovery hints".
Audit before stabilizing. Confirm the five kinds cover what third-party
bridges need to say about a rejection (installation was deliberately left to
provider/installation/*). Decide whether retryable should be per kind
(only sessionArchived and rateLimited read it today) and whether the
runtime should bound the rateLimited ladder from the hint rather than from
a constant.
Provider maintenance toolkit (experimental_resolveExecutablePath, experimental_readCliVersion, experimental_commandOutput, experimental_versionFrom, experimental_compareVersions, experimental_formatCommand, experimental_npmCommand, experimental_npmGlobalInstallCommand, experimental_npmLatestVersion, experimental_probeNpmGlobalPackage, experimental_npmGlobalInstallSource, experimental_installationVerification, experimental_downloadedInstallerCommand, experimental_clampPercent) (@get-bb/plugin-sdk/provider-bridge)
These are the host-local probes and install commands behind a bridge's
provider/health, provider/usage and provider/installation/* answers for
a user-installed CLI.
- Probes:
resolveExecutablePathreturns an absolute path or null, and on Windows prefers a PATHEXT match such as an npm.cmdlauncher.readCliVersionruns--versionwith stdin closed and returns the first valid SemVer token or null.commandOutputreturns trimmed stdout+stderr or null. The others areversionFrom,npmLatestVersionandprobeNpmGlobalPackage. - Decisions:
compareVersionsuses SemVer precedence and throwsTypeErroron invalid input.npmGlobalInstallSourcereturnsnpmGlobal,externalornotInstalled.installationVerificationpasses an install on existence, and an update on reaching the latest version (or any change when the registry was unreachable). - Actions:
npmGlobalInstallCommand;downloadedInstallerCommand, which runs a vendor script from a temp file and, givenpowershellUrlon Windows, returnsirm <url> | iex;npmCommand;formatCommand; andclampPercent.
Audit before stabilizing.
- Timeouts are fixed. 5 s for
which/--versionand 15 s for npm and self-diagnostics, so a bridge whose CLI is slow to start (a JVM, a first-run download) cannot lengthen them. Decide whether the budgets become arguments before the signatures are a promise. compareVersionsthrows. Confirm the throwing contract for invalid inputs before stabilization.- The npm helpers assume a global install.
probeNpmGlobalPackageandnpmGlobalInstallSourcemodel npm's global prefix only; pnpm, volta and corepack shims read asexternal. Decide whether the source enum should grow before it is relied on. downloadedInstallerCommandwithoutpowershellUrlis POSIX. On win32 it still returnssh -cwithmktemp,curlandbash. Decide whether it should refuse there rather than hand the daemon a command that cannot run.
Presentation builders (experimental_presentationTitle, experimental_presentationDetail, experimental_withTitle, experimental_presentationFileName, experimental_COMPACTION_PRESENTATION, experimental_REASONING_PRESENTATION, experimental_fileReadPresentation, experimental_searchPresentation, experimental_webSearchPresentation, experimental_webFetchPresentation, experimental_planStepsPresentation, experimental_toolPresentation) (@get-bb/plugin-sdk/provider-bridge)
These build the grammar-v3 presentation a bridge stamps on every item
(provider-plugin-api.md §3 owns the vocabulary).
presentationTitle takes the first non-empty line capped at 160 characters,
or undefined. presentationDetail caps a detail at the persisted schema's 280
characters. withTitle stamps a title only when there is one, and
presentationFileName returns the last path segment. The two constants carry
bb's fixed wording for compaction and reasoning rows. The five builders shape
file read, search, web search, web fetch and plan-steps rows, and
toolPresentation is the generic "Running/Ran " row.
Audit before stabilizing.
- The wording is a product decision. A bridge that adopts the constants inherits bb's English labels and glyph names. Decide whether the labels should come from the host (localized, themed) rather than be persisted from the bridge before the constants are a promise.
- The caps are the schema's. Stabilizing the helpers freezes the 160-character headline cap (a kit convention) and the 280-character detail cap (the persisted schema's) as public behavior.
- Plan-steps rows collapse by default.
experimental_planStepsPresentationsetssuppress: true(the todo banner reads the snapshot); codex keeps its own uncollapsed variant. Decide which default a third-party bridge should get.
A newline-delimited line reader. It frames on LF only (never readline, which
also splits on U+2028/U+2029) and strips a trailing CR. A line over
maxLineBytes (default 64 MiB) is discarded up to its terminator and reported
through the required onOverflow with its byte count. The optional onClose
fires after a final unterminated line is emitted.
Audit before stabilizing.
- The default cap is the wire's.
maxLineBytesdefaults to the JSON-RPC line bound the runtime applies to the bridge itself; a provider child's payloads may deserve a different bound. Decide whether the default should be an explicit argument for child pipes. onOverflowis required. A bridge might reasonably want to fail the session instead of logging the drop. Decide whether the reader should offer a fail-closed mode before the signature is a promise.
Live-file navigation (experimental_FileLink, BbNavigate.experimental_openFilePreview, and BbNavigate.experimental_openFileExternally)
Kept experimental (2026-08-22). Windows/UNC paths were never verified.
experimental_FileLink and experimental_openFilePreview open explicit live
workspace, host and thread-storage file targets through the current surface's
shared file-tab controller. experimental_openFileExternally opens a target
in the client's preferred external app. The boolean results report host
acceptance, and malformed targets stay inert.
Core callers. usePanelFiles owns file opening on the thread view, New
thread screen, and plugin page: core's workspace, host, and storage opens and
openFilePreview build the same tab requests under one scope rule, which
usePanelFiles.test.tsx checks against every surface. Links route through
usePanelBrowser on every surface.
Docs: frontend-components.md, frontend-hooks-and-ui.md.
Audit before stabilizing.
- Verify strict target/path/location validation on POSIX, Windows drive, and UNC paths, including stale environment, host, and thread identities.
- Confirm preview identity, persistence, opener preference, one-off Open with, disabled opener fallback, and explicit-host migration on Thread, New-thread, Settings, and plugin-page surfaces.
- Audit external opening across local and remote clients, disconnected hosts, missing preferred apps, and targets with line but not column support.
- Confirm link anchor behavior, unavailable menu states, copy semantics, and whether per-app external choices should remain host-owned menu affordances rather than become plugin-selectable API.
- Measure the lazy boundary: mounting a file link must not start file reads, preview imports, editor discovery, or panel-destination loading.
- Decide whether Git snapshots or deleted working-tree files merit separate target variants; do not weaken live-file guarantees to accommodate them.
Shows a terminal the plugin created with useSdk().terminals.create in the
current surface's terminal panel. A thread surface accepts only that thread's
terminals, the New thread screen only its current terminal scope, and a
plugin page any terminal. It resolves false for unknown or exited terminals
and surfaces without a terminal panel, and closing the tab closes the
terminal. Requested in #1132.
Core callers. This API calls usePanelTerminals's open; core's Start
terminal row, the terminal.open shortcut, and terminal tab selection use
its select on every surface. usePanelTerminals.test.tsx runs one contract
table against the thread view, New thread screen, and plugin page.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Decide whether plugins need tabs that hide without closing the terminal
(#1132's
closeBehavior: "detach"). Thread and New-thread surfaces derive tabs from live sessions, so this needs a hidden-session notion in panel state rather than a flag on the call. - Confirm the per-surface acceptance rules with a real consumer, including environment terminals opened from a thread view and host-path terminals whose cwd differs from the New thread screen's target.
- Verify compact-viewport drawer reveal, split panes, and opening a terminal whose host is disconnected.
- Decide whether a nav panel should also declare a default terminal scope so the native "+ Terminal" button follows the page's worktree.
Host plugin foundation (bb.hosts.experimental_client, ExperimentalHostClient.experimental_onWorkerExit, ExperimentalHostClient.experimental_onSignal, ExperimentalHostRpcContext.experimental_retainWorker, experimental_defineHostEntry, experimental_killProcessesWithCwdUnder, experimental_readProcessIdentity, and experimental_createHostEntryHarness)
Kept experimental (2026-08-22). Signals and watches have no consumer
(decide whether to delete them or keep them experimental separately from
calls). None of the lifetime/limit numbers has been measured against a plugin
other than keep-awake. The artifact-contract names (experimental_apiVersion,
experimental_signals, the injected context members) are read by the daemon
from installed artifacts, so renaming them needs a dual-name window plus a
protocol bump.
A plugin declares one bb.host Node entry with
experimental_defineHostEntry, shares a Standard Schema contract with its
server, and calls methods on an explicit enrolled host through
bb.hosts.experimental_client. The client can observe unexpected worker exits
and ephemeral host signals. Handlers can hold retention leases, and idle
workers are evicted after five minutes. experimental_killProcessesWithCwdUnder
reaps processes working under any of the given directories before they are
deleted (the SDK still accepts the pre-0.6.27 { directory } shape at
runtime), and experimental_readProcessIdentity(pid) returns
{ command, startedAt } so a caller can confirm a recorded PID before
signalling it.
Docs: backend-foundation.md, testing.md.
Audit before stabilizing.
- Process reap and identity. Confirm
experimental_killProcessesWithCwdUnder's platform coverage (Linux/proc, macOSlsof) and whether the grace should be per call. Confirmexperimental_readProcessIdentity's start-time precision per platform and whether a combined verified-stop helper should replace it. - Call timeout.
ExperimentalHostCallOptions.timeoutMsdefaults to 30 s and is capped at 30 minutes. Confirm the cap, and whether the default should be declared per method on the contract instead. - Contract shape. Confirm Standard Schema values remain the right runtime boundary and decide whether method-specific typed errors are necessary.
- Targeting. Confirm explicit host ids are enough for V1 and add an environment-aware primitive only alongside a plugin that proves its locking and workspace semantics.
- Process lifetime. Measure whether five idle minutes is the right timeout, whether watches should continue retaining automatically, and whether leases acquired only during active handlers are expressive enough. Confirm there is no need for manifest lifetime flags, plugin-selected timeouts, a global worker limit, or plugin-specific restart policy. Confirm unexpected-exit notification is the right generic repair trigger, remains suppressed for graceful and idle disposal, and does not create crash loops. Verify reconnect generation reconciliation covers disable/uninstall during an outage without stopping a still-current worker on every transient drop.
- Signals and watches. Confirm host signals should remain private, ephemeral invalidations rather than a durable event log. Audit native-watch coalescing, backpressure, rescan/error events, per-worker limits, and cleanup against a plugin that watches real workspace state.
- Paths. Confirm the stable host data path layout and generation-temporary cleanup behavior across crashes and daemon restarts.
- Limits. Audit the common call duration, startup/cancellation grace, 8 MiB JSON payload cap, 256 MiB artifact cap, and per-plugin admission limits (256 active calls / 32 MiB of active inputs) against real plugins. Confirm retaining only the most recently materialized artifact digest per plugin is sufficient.
- Environment. Confirm executable discovery through normalized
PATHand stripping all daemon-ownedBB_*variables. - Trust and dependencies. V1 host plugins are trusted Node programs that
may use
child_process, filesystem, and network APIs. Decide whether later permissions, native artifacts, or an explicit dependency installer can be layered on without changing the RPC contract. Confirm rejecting all private@bb/*imports from host bundles is the correct permanent boundary, and audit the builder-supplied public SDK runtime against future host exports. - Composition boundary. Confirm host RPC methods and signals should remain
private to the owning plugin while allowing another daemon subsystem to
consume the same
bb.hostartifact through its own bootstrap and lifecycle. - Test harness. Audit both layers. The server harness has the
experimental_callHostRpc,experimental_hostEntryandexperimental_declaredIconNamesoptions, theexperimental_hostRpcCallsinspection list, and theexperimental_emitHostWorkerExitandexperimental_emitHostSignaldrivers. The host-entry harness hasexperimental_call,experimental_getSignals,experimental_getRetainedWorkerLeaseCount,experimental_lifecycleSignal, path/watch options, andexperimental_dispose. Confirm the host harness should continue simulating validation, cancellation, lifecycle, JSON, and size limits without pretending to model process startup, crashes, native watcher recovery, or reconnect behavior.
Fixed-tab targets (experimental_target, experimental_useAppPanel, and experimental_useFixedTabTarget)
Kept experimental (2026-08-22). PluginNavPanelRegistration.fixedTabs
itself is stable. The target trio has one example consumer, and the
validator, survival and cross-thread items below are unanswered.
A fixed tab's experimental_target validator owns a JSON-safe target type.
experimental_useAppPanel() selects one of the calling plugin's eligible tabs
on the current surface and can submit a target.
experimental_useFixedTabTarget() returns the validated current-session
target with a sequence and clear(). Targets are memory-only: they survive
remounts but not a refresh.
Docs: frontend-core-slots.md, frontend-components.md.
Audit before stabilizing.
- Audit registration objects as references: identity is scoped to the mounted plugin and current nav panel, with no cross-plugin addressing or global ids.
- Confirm sync type guards remain the right owner validation contract and define error reporting if a validator throws or becomes stale after reload.
- Exercise repeated equal targets, explicit clearing, crashes, inactive-tab, panel, and route remounts, refresh, and compact drawer animation. Targets must survive remounts in the current app session, never survive refresh, and never reappear after their owner clears them.
- Decide whether a future cross-thread surface should navigate before opening;
the initial public surface intentionally supports only
{ kind: "current" }. - Keep core and plugin destinations on the same resolver and verify the controller never learns Changes, file, task, or document target shapes.
Added 2026-10-15 (SDK 0.6.53), replacing experimental_sidebarAccessory.
One consumer: the Tasks plugin's open-task count.
A nav panel hook returning ExperimentalBadge
({ count?, text?, tone?, label }) or null. count and text are
exclusive; text is at most 8 characters; tone is neutral (the default),
warning, or critical, and bb owns the colors. The sidebar navigation model
runs the hook once per registered panel in the plugin's slot scope and crash
boundary and hands the value to every navigation provider as item.badge.
The bundled Navigation plugin's Rail draws a count (capped at 9+) or a dot
on the icon, and does not draw text; its Top list and More menu draw the count
(capped at 99+), the text, or a dot. Both add label to the accessible name
and put a dot on More, in the strongest hidden tone, while a badged panel is
hidden there.
The fields follow the 30 bb-community plugins that drew
experimental_sidebarAccessory (October 2026): 12 drew one count, 8 several
numbers or a fraction, 5 a short status word, 3 a percentage, and 18 used
red, amber, or an accent color for state. A plugin that still passes
experimental_sidebarAccessory keeps loading; the field is dropped with a
warning. Remove that compatibility once released plugins have migrated.
Docs: frontend-core-slots.md.
Audit before stabilizing.
- Core parity. Decide whether a core destination needs a badge and, if
so, route it through the same
item.badgepath the Navigation plugin draws. - Tone and text. Confirm three tones and an 8-character
textcover migrated plugins, including the six that drew several colored counts and now draw one badge. - Hook lifetime. The hook runs for every registered panel while the sidebar is mounted, including hidden ones. Confirm that cost is acceptable for plugins with expensive queries, or add visibility to the hook's inputs.
- More summary. The More button's accessible name lists every hidden badge. Check that this stays usable with many badged hidden panels.
Restored 2026-10-15 (SDK 0.6.53) with placement. It was removed in
#5229 (0.46.0); the names are unchanged so plugins built before 0.46, such as
Compact Nav 0.1.x and Sidebar Sections, load again.
What it does. Draws the sidebar navigation: New thread, Search threads,
Plugins, Skills, Automations, plugin panels, and More, plus optional controls
in the sidebar header. Exclusive.
sidebar.navigationProvider picks the provider by plugin key: the bundled
Navigation plugin's navigation/rail or navigation/top-list, another
plugin's key, or __automatic__ (the default: the first registered provider
from another plugin, else navigation/rail). Legacy __builtin__ and
navigation/navigation read as __automatic__.
The registration declares placement, rail or list (the default). bb lays
the sidebar out from the placement before the provider renders, and keeps the
layout around it: for rail, a fixed-width column that stays when the sidebar
collapses and Settings at the bottom of the column; for list, the area
above the thread list, a Settings footer action,
Back to app rows on the Settings, Plugins, and Skills lists, and a collapse
that hides the whole sidebar. Both placements use the same framed macOS
window chrome on wide viewports, so changing placement does not move the
window controls, history controls, or the vertical position of Settings search.
The component receives isCompactViewport,
placement, and the deprecated experimental_Original, which renders the
Navigation plugin's provider for the same placement, or nothing when that
plugin is disabled.
The optional headerComponent renders in the sidebar header row while the
navigation is active. On wide macOS desktop windows, this row sits below the
window title bar; elsewhere it sits between the sidebar toggle and bb's back
and forward buttons. It receives width, controlSize,
and isCompactViewport. The host clips it to the row, keeps the macOS drag
region for non-interactive space, aligns it with the macOS window controls,
and on a crash draws the header of the Navigation plugin's provider for the
same placement, with one toast. It replaces the separate
experimental_sidebarHeader slot and its sidebar.headerProvider setting,
which let users pair a header with a navigation it did not belong to. That
slot remains as an untyped runtime alias: a plugin's header joins each of its
navigations that has no headerComponent.
Before plugin frontends settle, a pinned provider, or the provider Automatic last chose, reserves the placement it last had and draws nothing until it registers; Automatic with nothing remembered reserves the rail. Once frontends settle, a missing pinned provider falls back like Automatic, and with no provider at all the sidebar uses the list placement and says no navigation plugin is enabled. A crashing provider is replaced by the Navigation plugin's provider for its placement, with one toast.
Core callers on the same path. bb's Rail and Top list are the bundled,
default-enabled Navigation plugin (plugins/navigation), which is on the
forkable list. It registers both through this slot with no host-only API:
the host owns only the placement layout, Settings, the customize editor, and
the navigation model. Rail's header is its New thread button, registered as
headerComponent; it does not read the size props.
Audit before stabilizing.
- Placements. Confirm two placements cover real providers, or whether a rail provider needs a declared width.
- Boundary. Verify providers can express useful navigation without the drawer, thread list, footer, resize handle, Settings, or shortcuts.
- Arbitration. Confirm Automatic preferring the first installed provider from another plugin over the bundled Rail is right, including when several are installed.
- Loading. Confirm an empty region in the remembered placement, now also for the bundled Navigation plugin at every boot, is acceptable.
- Original. Remove
experimental_Originalonce released plugins stop rendering it. - Header alias. Remove the
experimental_sidebarHeaderalias once released plugins passheaderComponent.
experimental_useSidebarNavigation, experimental_useSidebarNavigationSplit, experimental_SidebarNavigationIcon (@get-bb/plugin-sdk/app)
Restored 2026-10-15 (SDK 0.6.53).
What it does. experimental_useSidebarNavigation() returns
{ items, activeItemId, isHomeActive, isShortcutModifierHeld, actions } from
one model mounted above the sidebar. Items arrive in the user's saved order
with isVisible, isLoading, pluginId, shortcut metadata, and badge (the
panel's experimental_useBadge value). Plugins written before 0.46 that drew
the removed experimental_Accessory item field show no counts until they read
badge. Item ids are the keys stored in
sidebar.pluginPanelOrder and sidebar.visiblePluginPanels, and items keep
their identity while unchanged. actions covers
activate(id, { openInSplit }), goHome(), setVisible, setOrder (unknown
ids are dropped and omitted ids keep their relative order at the end),
openCustomize() (bb's editor in a popover beside the navigation, after open
menus close, returning focus to the opener), openDetails, and
disablePlugin. activate(id, { openInSplit: true }) opens Plugins and Skills
in a browser tab synchronously from the user gesture; New thread and plugin
panels retain their existing split, compact-screen and pane-limit rules.
Search remains a command. Native shells retain their window-open policy.
Calls after the caller unmounts do nothing; outside the
sidebar the hook returns no items and inert actions.
experimental_useSidebarNavigationSplit(id, options?) mirrors
experimental_useSidebarThreadSplit and accepts activation: "distance" and
onDragStart for rows in menus. experimental_SidebarNavigationIcon renders
bb's glyphs and panel icons with plugin branding as fallback.
Core callers on the same path. The bundled Navigation plugin's Rail, Top list, and header New thread button use these hooks; the app's Settings → Appearance → Navigation picker reads the same registrations.
Audit before stabilizing.
- Semantic items. Confirm the action and icon variants cover current navigation without exposing routes or host elements.
- Home. Confirm
isHomeActiveandgoHome()are the right Home contract for rail providers, including after Settings closes the compact drawer. - Order semantics. Confirm
setOrder's partial-order rule works for drag-and-drop providers. - Accessibility. Validate labels, badges in accessible names,
aria-current, shortcut metadata, and focus return from Customize in third-party markup.
Kept experimental (2026-08-22). The collapsed-section status rollup consumes it.
Lets a plugin-lifetime content script set or clear one of its own status indicators on an explicit thread row. The status survives route changes and clears when that frontend generation deactivates.
Docs: frontend-core-slots.md.
Before stabilization, audit:
- whether explicit thread targeting belongs on content-script context or a dedicated app-level controller;
- multiple simultaneous runs owned by one plugin on one thread;
- arbitration across plugins, frontend generations, and native thread statuses;
- persistence expectations across full app reloads and multiple windows;
- validation, accessibility labels, reduced motion, and cleanup on plugin reload/disable/removal;
- the name of
PluginComposerThreadRowStatus.tone. The field is a state the plugin reports (default | running | success | error) that the host maps to both a color and an animation, sostateis the candidate rename. Nothing underplugins/*sets a status today, so the rename is free until the prefix drops.
bb.providers.register (experimental_bridgeOptions, experimental_visibility, and the experimental_providerBridge artifact export)
Kept experimental (2026-08-22). bb.providers.register and the
declaration's target-state fields are stable. experimental_bridgeOptions and
experimental_visibility have one consumer (the ACP plugin); decide whether
static options survive beside deriveProviderOptions before naming them. The
daemon bootstrap reads the experimental_providerBridge export name from
every installed plugin. Renaming it needs a dual-name acceptance window plus a
protocol bump, so it stabilizes with the bridge kit once that deprecation
policy exists.
experimental_bridgeOptions is bounded JSON, opaque to core, delivered to
every bridge launch as provider-scoped static options.
experimental_visibility: "installed" withholds a provider from unscoped
listings until its provider/health is not not_installed. The bridge is the
experimental_providerBridge export of the plugin's bb.host artifact.
Docs: providers.md, provider-plugin-api.md §1–2.
Audit before stabilizing.
- Install-order ranking. Bundled plugins rank by their position in
BUNDLED_PLUGINS; other plugins byinstalledAt. Confirm that a reinstalled builtin (tombstone then reinstall) keeping its bundled rank is right, and thatinstalledAtis the fact users expect for third-party order (vs. first-load time). The user overlay (providerOrder) is an ordered id list that ignores unknown ids; decide whether stale ids should be pruned on write. - Icon URL shape. A path or declared icon is snapshotted at registration
and served from
/api/v1/system/providers/<id>/logo; a host glyph name yields a nulllogoUrland no server-side resolution. Decide whether the host should resolve declared glyph names for providers the way it does for plugin branding, and decide the logo route's cache policy, before either freezes into clients. - Collision semantics. Ids are first-come collision-rejected: a staged collision fails the whole plugin load; a post-activation registration throws to the plugin. A disabled first-party plugin leaves its id claimable by anyone until it is re-enabled (which then fails to load). Confirm first-wins is right across plugin load order, and decide whether a namespace rule (plugin-scoped id prefixes) is wanted before third-party ids proliferate.
- Bridge delivery. Every provider bridge arrives as the plugin's one
bb.hostartifact. Confirm one artifact per plugin survives (a plugin declaring several providers ships one bridge for all of them, and there is no way to name a second), confirm the single-bundle shape survives per-platform needs, and decide whether a router-kind declaration (a picker entry resolving to another provider at submit time) returns as its own surface. - What a capability may be. Apply this test to every remaining capability before stabilizing: a declaration may assert what the provider itself implements and an external consumer needs pre-session, never what bb or its daemon can do with it.
- Static bridge options and visibility. Confirm 64 KiB remains a suitable
declaration-time limit, that opaque options should continue to be shared by
every host rather than resolved per host, and whether deep-frozen plain JSON
is the right stable value contract. Confirm
"always" | "installed"is enough listing policy, that health failure should continue to hide an installed-only provider, and that targeted requests may continue resolving a registered provider even while discovery says it is absent.
Kept experimental (2026-08-22). experimental_defineProviderBridge /
experimental_apiVersion are an artifact↔daemon contract (the bootstrap
refuses anything but version 1 by name), and the deprecation window between
independently-updating artifacts and daemons (item 2) is undecided.
The published module a provider bridge compiles against. It holds
experimental_defineProviderBridge, the Provider Bridge Protocol's methods,
the thread/delta grammar and param schemas, the bridge kit's helpers, and
the @bb/domain command-plane vocabulary those params reference. It is
curated by hand with named exports only, and bundled into the plugin's
artifact rather than stubbed at runtime.
Docs: providers.md, provider-plugin-api.md §2,
provider-bridge-protocol.md.
Decided (Aug 2026): bridges emit the protocol's own thread/delta grammar,
and the @bb/domain command-plane, interaction and enum types they reference
stay re-exported through the facade permanently.
Audit before stabilizing.
- Surface size. 184 names. Any further shrink is a per-name product decision, not a mechanical move.
experimental_apiVersion1. The bootstrap accepts version 1 only and refuses anything else by name. Decide the deprecation window for a version bump (a plugin's artifact and the daemon update independently) before the first third-party bridge ships.
Kept experimental (2026-08-22). Items 2 and 5 below change the public shape (a pluggable replay child, pinning a grammar version in the exports).
The published kit a bridge author proves a bridge with, with no private
@bb/* package in reach. It contains the conformance runner
(experimental_runBridgeConformance), the real delta assembler, the
delta→event collector, the in-process JSON-RPC harness, the calibration
normalizer, and the recorded-replay harness and readers. It is
framework-agnostic and exports named members only.
Docs: providers.md.
Audit before stabilizing.
- Calibration normalizer scope.
experimental_normalizeCalibrationEventsinterns the id fields the first-party goldens needed (turnId,itemId,id,parentToolCallId) and dropsproviderCheckpointId. Confirm the defaults against a third-party bridge's goldens before fixing them. The JSON-RPC harness duplicates a little of the bridge kit's envelope parsing; fold or keep it deliberately. - Replay profile shape.
ReplayProviderProfileis the three seams the first-party bridges needed (env,rewriteRuntimeLine,prepareState) andReplayDialectis the three protocols the replay child speaks (json-rpc,claude-cli,pi-rpc). A third-party bridge whose CLI speaks none of them cannot replay its provider lanes. Decide whether the dialect set grows, or whether the child becomes pluggable, before the profile is a promise. - Two shipped programs. The kit's bundle spawns
provider-bridge-worker-entry.mjsandreplay-provider-child.mjsfrom beside itself, so the published package carries both underdist/. Confirm the bundled bootstrap tracks the daemon's; a drift would make a replay differ from production in argv or framing. - Workspace-path restoration.
experimental_replayRecordingrewrites the replay's temp workspace back to the recordedcwdin every line the bridge emits, by textual substitution. Confirm no bridge emits that path in a form the substitution misses (URL-encoded, JSON-escaped backslashes on Windows). - The canonical event vocabulary by name. The kit exports
ThreadEvent,ThreadEventItem,ThreadEventItemPresentationand the named item kinds as types only. The persisted vocabulary therefore has a second public home besideProviderInfo, and a breaking change to an item shape is a breaking change to the kit. Decide whether the kit should pin a grammar version in its exports (the assembler already namesASSEMBLER_GRAMMAR_VERSIONS) before stabilizing.
Scans a plugin package for imports outside the public SDK and an allowlist.
It returns the files read and each offending specifier: a private @bb/*
package, outside-package, dynamic-specifier, and @bb/* package.json
dependencies. The suite asserts on the result.
Docs: testing.md.
Audit before stabilizing.
- The allowlist is bb's. The default admits every published SDK subpath
and
vitest; a plugin on another runner or another schema library must name it inallow. Decide whether the defaults should read the plugin's own package.json dependencies instead of a fixed list. - Tokenizer, not a parser. A dependency-free tokenizer skips comments,
strings, template literals, regular expressions, and JSX text, then reads
import/export … fromdeclarations andimport()/require()calls that are not property accesses or method definitions. Lexing is heuristic where JavaScript is ambiguous without a grammar: a/after)is read as division, and a<in a.tsxexpression position is tried as JSX and falls back when no closing tag matches. Decide whether a full parser is owed before the scan is a promise.
Kept experimental (2026-08-22). The hook returns ProviderInfo, which
carries the unresolved icon / logoUrl pair and maintenance. Stabilizing
the hook freezes that shape.
Returns { status, providers }, the host's ProviderInfo[] roster in picker
order with shared cache and realtime invalidation.
Docs: frontend-registration.md,
provider-plugin-api.md §5.
Audit before stabilizing.
- Routing. The hook reads the primary-host roster (no
environmentId/hostIdargument), so installed-only providers of another machine are not listed. Decide whether plugins need host-scoped listing before freezing the signature. - Icons. A provider's SVG asset arrives as
logoUrl(drawn as acurrentColormask) and a named glyph asicon: { glyph }; at most one is set. Decide whethericonandlogoUrlfold into one{ glyph } | { url }field whenProviderInfostabilizes, and whether the monochrome mask rendering is the contract or a full-color logo path is owed.
Returns { mode, name, theme }: the active light/dark mode, the code theme
name bb renders it with, and the resolved VS Code theme document, for plugins
that render code with their own engine.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Shape of the document.
PluginCodeThemeDatamirrors Shiki'sThemeRegistrationResolvedminus the fields bb does not promise. Decide whether freezing a Shiki-shaped payload is right, or whether the contract should be bb's own normalized token model; a Shiki major that changessettingsnormalization changes what plugins receive. - Both modes at once. The hook serves only the active mode. Decide whether the state should carry the light/dark pair.
- Where the resolve happens. The document is resolved in the app window and cached per theme name for the window's lifetime, so a custom palette edited on disk keeps its old document until reload. Confirm that matches what the built-in surfaces do.
- Consumer count. One consumer today. Confirm a second engine (CodeMirror, xterm) needs the same payload before the prefix drops.
A plain function, ({ text, html? }) => Promise<boolean>, over the writer
bb's own copy actions use: bb Desktop's native clipboard, then the Clipboard
API, then execCommand("copy"). It resolves true only once a path wrote the
content and never rejects. The test harness records writes in the most
recently rendered slot's inspection.experimental_clipboardWrites and
resolves with that slot's experimental_copyToClipboard render option
(default true).
Core callers. The export is copyToClipboard from
apps/app/src/lib/clipboard.ts. Core copies call it directly or through
copyToClipboardWithToast and useClipboardCopy: CopyButton, the message
action bar, file preview, terminal, and the Copy thread link thread action.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Content shape. Only plain text and HTML are supported. bb's message copy also writes an image, through its own path with Android and browser variants. Decide whether images belong in this contract or stay core-only.
- Harness scoping. The clipboard is global, so the harness routes writes to the most recently rendered slot. Confirm that is enough for tests that render several slots, and decide whether content-script tests need their own capture.
- Desktop bridge failures. A rejected bridge write falls through to the browser paths, which can then fail for the reasons the bridge avoids. Confirm on Linux Wayland and X11 that the bridge write lands.
- Feedback. Every caller pairs the result with a toast or a manual-copy fallback. Decide whether a shared toast helper belongs in the SDK.
experimental_saveFile({ fileName, url } | { fileName, blob }) starts a
download through the same function bb's own downloads use. It clicks a
temporary download anchor, using an object URL for a blob. The bb iOS shell
downloads through WKDownload, keeping the WebView's request context, then
presents the share sheet; file bytes and cookies do not cross the JavaScript
bridge. The promise resolves started once the link is clicked or failed,
with an error toast, if it could not start. Completion and share-sheet
cancellation are not reported. The test harness resolves started and records
nothing.
Core callers. The export is saveFile from apps/app/src/lib/save-file.ts,
which file preview downloads, chat attachment links in the native shell
(ConversationAttachments), and voice-input recording downloads call. The
Tasks plugin's attachment cards call the SDK export.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Outcome shape. Decide whether callers need completion or cancellation
beyond the browser-compatible
startedresult. - Android. The native download implementation is iOS only. Decide on Android file sharing and browser-created blob download support.
- Harness inspection. Decide whether the harness should record requests the way it records clipboard writes.
- Link helper. Decide whether the repeated click handler belongs in a shared link component.
Returns the id of the plugin that owns the calling component, the same value
as bb.pluginId.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Hook or setup value. Code that runs in
definePluginApp's setup, not in a component, cannot call a hook. Decide whether the builder should carry the id as well, or instead. - Scope. The hook throws outside a plugin slot component. Content scripts
already receive
context.pluginId; confirm no other frontend entry point needs it. - Consumer count. One consumer today. Confirm a second plugin needs it before the prefix drops.
Returns the remappable answer shortcuts bb binds while a pending interaction is
open, keyed by option index, and registerChoiceHandler for a chosen index.
Outside a pending interaction the map is empty and handlers never run.
Docs: frontend-hooks-and-ui.md.
Audit before stabilizing.
- Props or hook. Decide whether the shortcuts belong in
PluginPendingInteractionPropsinstead of a hook any component can call. - Scope. Only pending interactions bind the shortcuts. Confirm no other surface (a message action form, a panel) should get them.
- Consumer count. Two first-party consumers (Ask User Question, pi's extension dialogs). Confirm a third-party form needs it before the prefix drops.
Kept experimental (2026-08-22). There are no shipped registrations, because declared glyphs and SVG assets cover every first-party provider without a frontend bundle. The remaining questions concern bundle loading cost and rendering behavior.
Registers { providerKind, providerId, icon }, a React component that draws
one agent, environment, or machine provider's icon in place of its served
logoUrl. Exact-kind registrations beat all-kind ones.
Docs: frontend-renderer-slots.md, frontend-components.md.
Audit before stabilizing.
- Scoping and overrides. Cross-plugin icon overrides are allowed. Within each kind/id pair, the first claim by sorted plugin id wins and later claims warn. Verify kind isolation, specific-over-all precedence, and fallback after unload.
- Bundle size and boot ordering. An icon costs a frontend bundle. A provider plugin's bundle is deferred until a thread of its provider is opened, one of its forms is requested, or its panel route is visited, so until then the icon is absent from the picker and sidebar. Confirm the cost is acceptable for third-party icon-only plugins, or add a lighter delivery path (e.g. a declared inline SVG string sanitized by the host) before freezing the shape.
- Disposal and identity. Audit that a crashing plugin icon is contained the way other slot components are (it renders inside host chrome, sometimes outside a slot error boundary), and that no host surface caches the resolved component across a reload.
- Rendering contract. The host promises only
classNameand expects inline markup. Decide whether to enforce that (no fetches, no portals, no interactive content) before plugins rely on richer components, and confirm the accessible label story: the host derivesariaLabelfrom its own provider data, falling back to the provider id, and the slot supplies none.
bb's execution picker as a controlled
{ providerId, model, reasoningLevel, serviceTier? } component. It has
optional host/environment routing, allowProviderChange, align and
disabled, and the composer's catalog and normalization policy.
Docs: frontend-components.md.
Audit before stabilizing.
- Atomic controlled contract. Confirm the four execution fields should remain one value and that selecting a provider should immediately choose its verified default rather than wait for an explicit model click.
- Routing. Omitted
routingfollows the server's primary-machine routing;{ kind: "host", hostId }and{ kind: "environment", environmentId }cover machine- and workspace-dependent catalogs. Confirm those two routes are sufficient before stabilizing the discriminated union. - Capability policy. Confirm an unsupported provider should omit
serviceTierwhile supported providers retain the controlled tier, and that reasoning should continue using the composer's closest-supported policy. - Catalog failure and normalization policy. Placeholder, failed, and empty catalogs never change the controlled value. A verified catalog, possibly the machine's stored one while a refresh runs or keeps failing, normalizes stale values while selected-only retired models survive. Confirm silent retention on failure and automatic correction on success are preferable to explicit callbacks.
- Scope. Validate settings and compact-form usage in external plugins, especially whether they need an explicit loading/error callback. Tasks and Automations currently rely on the picker-owned loading/error UI.
bb's permission picker as a controlled
{ providerId, value, onChange, routing?, align?, disabled?, className? }
component. It applies the composer's supported modes, fallback order and the
routed machine's permission ceiling.
Docs: frontend-components.md.
Audit before stabilizing.
- Controlled reconciliation. Confirm automatic
onChangeafter an authoritative capability/ceiling change is preferable to a separate invalid-state callback, and that provisional failures should remain read-only without changing the caller's value. - Routing and provider coupling. Confirm requiring both
providerIdand the shared host/environment routing is the right composable boundary. A provider switch across two sibling controls settles in two controlled updates rather than one combined execution tuple. - Single-mode presentation. Confirm compact/settings consumers should see a locked summary when only one mode is supported, while first-party composer call sites may continue hiding a non-choice.
- Permission ceiling behavior. Audit hosts whose ceiling is below every mode a provider supports and decide whether the picker should render an explicit unavailable state instead of the controller's existing fallback.
Kept experimental (2026-08-22). There are no extension-row consumers. Every item is about the prop shape, so the first real renderer must precede stabilization.
Registers { kind, component } to render the expanded body of the plugin's
own extension rows ("<pluginId>/<name>"), of "tool" rows from its
providers, or of a requestInput form's history ("<pluginId>/<rendererId>").
The row header stays host-rendered, and a crash falls back to the declarative
base.
Docs: frontend-renderer-slots.md, backend-cli-agents.md (form rows),
provider-plugin-api.md §5.
Audit before stabilizing.
- Body versus whole row. Decide whether a plugin may also replace the
header (an inline widget with no disclosure, e.g. a goal card) before
freezing the prop shape, and whether
suppressshould stay a bridge-only decision or the renderer may opt a row back in. - Tool-row payload. A
"tool"row hands the renderer{ arguments, output }whereoutputis the server's inline preview for long outputs. Decide whether the renderer gets the full output on demand or only the preview. - Provider ownership source.
"tool"scoping reads the thread'sProviderInfo.pluginIdfrom the nearest thread provider context. A host surface that renders rows with no such context, and a provider whose plugin was uninstalled, both resolve to "unknown owner", so no renderer applies; confirm that is the right failure mode for both. - Legacy rows.
presentationis null on a tool row persisted before bridges attached one. Decide whether the renderer should see such rows at all, or only rows with a presentation. - Mobile parity. Mobile renders the declarative base and loads no plugin JS (by design). Confirm the base (label, glyph, tint, title, detail) is sufficient for the first-party extension kinds before a third party relies on a web-only upgrade.
- Form rows. Decide whether form and extension rows need distinct
kindvalues.
Kept experimental (2026-08-22). There are no consumers, and items 1 (a newly required create-thread field going missing silently) and 6 (projectless switching) need a consumer to validate.
The host-owned new-thread compose surface. It calls onSubmit with a
NewThreadRequest carrying every resolved selection (including
sessionOptions when the user changed a provider's agent option), and the plugin then
creates the thread with bb.sdk.threads.spawn, which owns filing and
attribution.
Core callers. The New thread screen (RootComposeView) renders the same
apps/app/src/components/promptbox/NewThreadComposer.tsx;
PluginNewThreadComposer adapts it for the SDK, and its round-trip tests
(PluginNewThreadComposer.test.tsx, new-thread-environment-seed.test.ts)
guard the seed mapping against the root composer's environment resolution.
Docs: frontend-components.md.
Audit before stabilizing.
NewThreadRequestvs. whatthreads.spawnaccepts. Confirm every field still round-trips throughbb.sdk.threads.spawnunchanged, thatexecutionInputSourcesstill means the same thing to the server, and that no newly required create-thread field is silently missing. The composer never reports aproviderIdprovenance source even though it always sends an explicitproviderId; decide whether that is correct before freezing the shape.- Page-level behavior the adapter skips. Fork seeds, quick-create-project, the guided machine-setup dialog, and codex-version submit blocking are deliberately absent. Confirm none of them has become load-bearing for correctness on a plugin surface. Without codex-version blocking, a plugin can submit to a machine whose CLI the primary surface would have refused.
- Draft and selection scoping. Drafts persist under
draftKey ?? pluginIdand execution selections are component-local, so a plugin panel never rewrites the user's root-composer defaults. Confirm that is still what plugin authors expect, and thatdraftKeyis the right knob (versus a per-instance ephemeral draft). - No plugin composer host binding. Plugin composer customizations,
banners, and
useComposer()writes do not reach it. Decide whether composers rendered by a plugin should participate in that surface before stabilizing. - Seeding props and the round-trip guarantee. The
default*props are uncontrolled seeds that re-seed on any value change, including user-touched selections.defaultEnvironmentcannot representproject-default,personalwithout ahostId, or anunmanagedpath. Confirm the seed mapping still inverts the root composer's environment resolution, and decide whether re-seed-on-change should instead be an explicit reset nonce. - Projectless contract. The picker always offers "Don't work in a
project", even when a plugin seeds a specific project; that choice submits
the personal-project id with a
personalworkspace. Confirm unconditional project switching is right for embedded plugin workflows, rather than adding an explicit project-locking policy.
Mounts a no-props plugin component once per app window, outside route-owned layout, with app-level SDK hook context. A crash hides only that overlay.
Docs: frontend-core-slots.md.
Audit before stabilizing.
- Name and boundary. Confirm "app overlay" is broad enough for floating widgets, launchers, and transient app-wide UI without inviting plugins to replace host-owned navigation or layout.
- App-level versus pane-level context. Define the selected route in split layouts and document which pane-local capabilities remain unavailable to a once-per-window owner, including composer and side-panel hosts.
- Host-owned layer. Decide whether arbitrary fixed/portalled content is sufficient or bb should provide a named overlay root, z-index band, collision area, docking, or drag persistence.
- Responsive and accessibility policy. Audit keyboard access, focus restoration, escape behavior, compact drawers, reduced motion, and whether any of those must become host-owned rather than plugin-owned.
- Multiplicity and budgets. Registrations are additive with no cap. Measure startup, query fan-out, visual collisions, and several plugins mounting persistent widgets in one window.
- Lifecycle. Verify exact once-per-window mounting across route changes, split changes, frontend reload, disable, uninstall, app teardown, and multiple desktop windows or browser tabs.
- Crash and stylesheet lifetime. Confirm a hidden crash fallback and the standard slot-owned CSS retention are the right failure semantics for UI that may have no in-layout representation.
Kept experimental (2026-08-22). There are no consumers, and item 5 is deferred until an external plugin adopts it.
Adds a row to the root New thread screen's right-panel Actions list. The row
can open a closable tab with { projectId: string | null, params: JsonValue | null }.
It is separate from threadPanelAction, which requires a threadId.
Docs: frontend-core-slots.md.
Before stabilization, audit:
- Surface naming. Confirm "New thread" remains the product name and the slot should stay panel-specific rather than becoming a broader root-compose action surface.
- Context breadth. Confirm the selected
projectIdis sufficient. A plugin can use the composer hooks for the live draft, but the slot does not expose the root composer's selected host, environment, provider, or model. - Project changes. An open tab receives the current project on every
render, while
runreceives the project selected when the row was activated. Confirm that distinction is intuitive and whether changing projects should close or re-key open tabs. - Persistence. Tabs and JSON params persist in the root panel's fixed state. Confirm restoring a plugin tab before registrations load, after a plugin is removed, and in projectless compose has the right fallback.
- Relationship to
threadPanelAction. Confirm separate opt-in remains preferable to a unified discriminated context after external plugins have had time to adopt the root surface. Decided: both contexts'openPaneltakePluginPanelActionOpenOptionsand returnboolean. Audit only whether the two contexts should merge.
Registers host-rendered icon items in the sidebar footer: an action with
onActivate, or a disclosure whose component is revealed above the row and
receives dismiss(). A disclosure registration returns an
open/close/toggle controller, and only one disclosure is active across
all plugins.
Docs: frontend-core-slots.md.
Audit before stabilizing.
- Naming and shape. Confirm
sidebarFooter,action, anddisclosureare the durable concepts, and whether the managed namespace should keep one discriminatedregistermethod or split registrations by behavior. - Imperative controller. Validate real plugins can issue open requests without leaking subscriptions across frontend generations. Decide whether a declarative external-store contract would be safer.
- Programmatic opening. Confirm
open()should remain available. The intended policy is direct user-driven flows; background changes should not surprise-open sidebar content. - Disclosure lifecycle. Components currently mount only while open and remount after dismissal. Confirm plugins do not require retained hidden state, or add an explicit retention policy before stabilizing.
- Footer capacity. Establish overflow behavior when several plugins register items, including compact viewports and icon-collapsed sidebars.
- Compatibility. Migrate representative users of
sidebarFooterAction, then decide whether stabilization replaces and deprecates that method or keeps action registration in both surfaces. - Focus and dismissal. Validate icon toggling, Escape, focus return, disclosure replacement, plugin reload, crash isolation, and removal while open across desktop and compact sidebar layouts.
Kept experimental (2026-08-22). Only examples use it. No shipped consumer has tested the arbitration/fallback model or the accessibility contract.
An exclusive slot that replaces the sidebar's scrolling thread list. bb's own
list is the bundled thread-list plugin. Automatic selection prefers another
registered list, and the user can pin one with sidebar.threadListProvider.
Docs: frontend-registration.md.
Audit before stabilizing.
- Arbitration. Confirm automatic/pinned, with installed lists preferred over the bundled one, is the right long-term selection model and alphabetical plugin-id order is an acceptable default tie-breaker when multiple replacements are enabled.
- Fallback discoverability. Confirm one toast plus the placeholder's Reload button is the right signal when the list crashes.
- Region boundary. The plugin gets the scrolling list and nothing else: the New-thread button, the navigation rail, and the footer stay host-rendered. Confirm no real sidebar needs to claim more, and that passing those regions down as props (letting a plugin place them, at the risk of dropping them) stays the wrong trade.
- Search compatibility. Confirm released plugins no longer need the
required deprecated
searchQueryfield before removing it in a deliberate breaking change. Until then, the host supplies"". - Accessibility. Confirm the host can still guarantee list semantics,
focus order, and the mobile close behavior when a plugin owns the markup;
onNavigateis currently the plugin's responsibility to call.
Registers { id, displayName, complete?, transcribe?, status? }. complete
serves thread titles and commit messages, transcribe serves voice input,
and status feeds pickers and Automatic. The user selects per task
(automatic, off, or { pluginId, serviceId }), and an explicit pick
never falls through to another service.
Docs: backend-cli-agents.md, provider-plugin-api.md §7.
Audit before stabilizing.
- Structured input. Confirm a bare prompt string stays enough, or whether services need the task (title vs commit) or a length hint without breaking the "plugin owns the model" split.
- Status freshness. The picker and the microphone can show a status up to 10 s stale, and a task past that age waits up to 2 s for a fresh one; a plugin whose readiness flips (sign-in, quota) might want to push a change instead of waiting for the next poll.
- Voice payloads.
transcribereceives the wholeFilein process (25 MB cap). Decide whether streaming matters for long recordings. - Automatic order as policy. Core prioritizes bb cloud, then sorts by plugin id and service id, including third-party plugins. Decide whether this order and automatic inclusion should become user-editable settings.
- Several services per plugin. Confirm the id-per-registration shape and the per-plugin id scope (plugin id plus service id).
Kept experimental (2026-08-22). It is persisted in opener-tab paramsJson,
so a rename needs a read-compat shim. Items 3–4 decide whether the stable name
is hostId? or a required field.
Names the explicit host for a project-backed workspace file that a file opener cannot route through a thread or environment. It is omitted for the other source kinds and for the primary host.
Docs: frontend-core-slots.md.
Audit before stabilizing.
- Confirm an explicit host id is the minimum missing project-routing context, rather than exposing the whole project workspace routing union.
- Verify project-compose file tabs retain the selected host across reloads, host changes, plugin fallback, and per-open viewer overrides.
- Decide whether host identity should be present for every source kind or remain project-specific once more file opener plugins exercise the API.
- Confirm omission should continue to mean primary-host resolution, that this
remains compatible with persisted opener tabs created before the field
existed, and whether it can become a stable required
hostIdwithout breaking older opener implementations.
What it does. A host-owned controlled textarea with bb's voice input. It
takes value, onValueChange, and an optional onVoiceInputActiveChange;
every other textarea attribute, including ref and className, reaches the
underlying <textarea>. The caller styles the textarea; the host wraps it in
a relative container and, when the browser supports voice input, adds bottom
padding and the microphone, waveform, cancel, and stop controls. Finished
transcripts are appended to value through onValueChange and stay editable;
nothing is submitted. onVoiceInputActiveChange is true from the start of
recording until transcription finishes or is cancelled, and false on unmount,
so a form can hold navigation and submission. Unmounting discards late
transcripts. It uses the same microphone preference, transcription service,
and error handling as the prompt box. Without voice support it renders the
plain textarea; the test harness renders that plain textarea too.
The registry's voice-input-textarea item re-exports it, and the registry's
question-form renders its free-text answer with it. Inside bb, the built-in
Ask User Question and pi plugins and bb's own question form reach the same
component through the @bb/shared-ui/voice-input-textarea module the build
shims.
Audit before stabilizing.
- Prop surface. Every textarea attribute passes through. Decide whether a narrower explicit list is the better contract, and whether callers need to style the host's wrapper (it is a plain block today, so a flex child cannot stretch it).
- Voice lifecycle. Verify cancellation, microphone permissions, mobile
capture, and late transcription isolation when the component unmounts or
its
valuechanges mid-transcription. - Unsupported hosts. The textarea renders without controls when voice is unsupported, with no reason shown. Decide whether callers need the unsupported reason or a way to hide the controls.
- Consumer count. One form (the shared question form, used by Ask User Question, pi, and bb's own questions). Confirm a third-party consumer before the prefix drops.
Kept experimental (2026-08-22). It has one consumer (the github plugin's
Diff), and items 2–4 all change the prop surface.
Host-owned renderers for supplied code. experimental_SourceCode takes text
and a path. experimental_Diff takes a single-file patch, a path and
optional experimental_fullFileContents ({ old, new }). They are the same
components bb's own surfaces use, so an active renderer replacement covers
both.
Docs: frontend-components.md.
Decided (Aug 2026): context expansion takes caller-resolved contents, not a loader callback.
Audit before stabilizing.
- Prop surface. Confirm content + path + presentation plus optional full
diff sides is the right minimal contract, and decide whether
classNamebelongs in it at all. A replacement never receives it, so aclassNamethat only styles bb's renderer is a quiet inconsistency. - Diff input shape. Confirm single-file patch text is the right currency. Multi-file patches, pre-parsed input, and per-hunk rendering are all things callers have wanted; none are expressible now.
- Language selection. Highlighting is inferred from
pathonly. Confirm an explicit language override is not needed before the names freeze, and that no implementation-library language union leaks in when it is added. - Worker pool. Thread panes and plugin nav panels provide bb's highlighting worker pool; homepage and settings sections do not, so a diff rendered there is unhighlighted. Decide whether the host should provide the pool at the component instead of the surface.
- Selection to chat. bb's own surfaces pass a selection-to-composer
handler that the public component withholds. Confirm plugins should reach
that through
useComposer()rather than a renderer prop. - Size and virtualization. Neither component caps input size or virtualizes. Audit against a plugin that renders a very large file or patch.
app.slots.experimental_sourceCodeRenderer / app.slots.experimental_diffRenderer (@get-bb/plugin-sdk/app)
Kept experimental (2026-08-22). There are no registrations, and "two slots or one" changes the registration shape.
Exclusive slots that replace bb's source or diff renderer everywhere it draws supplied content, including other plugins' calls to the public components. Automatic selection prefers a registration from a plugin the user installed over one bundled with bb (such as Monaco), then the first in slot order, and the user can pin bb's renderer or a provider per client. A crash falls back to bb's renderer.
Docs: frontend-renderer-slots.md.
Decided (Aug 2026): the pin stays per client; a crash swaps back to bb's
renderer silently and disables the slot for the session; the replacement is
global, other plugins' surfaces included; replacements receive
experimental_fullFileContents like the public component.
Audit before stabilizing.
- Arbitration. Confirm automatic/pinned/built-in is the right long-term selection model. The two renderers pin independently; confirm users do not instead expect one "code rendering" choice.
- Global reach as precedent. No other slot lets a plugin reach into another plugin's rendered output. Audit that before it is copied.
- Capability parity. Selection-to-chat and the deleted-file gate remain host-only. Confirm that asymmetry is acceptable, or promote either capability before stabilization.
- Two slots or one. Confirm source and diff should stay separately replaceable rather than one "code renderer" registration.
Renders fenced code blocks of the claimed languages in assistant and
nested-agent message Markdown, where messageDirective activates. The
component receives { code, language, status, index, message, Original },
mounts once per fence, and survives streaming: the host owns the instance and
portals it to a placeholder keyed by the fence's source offset, keeping
iframes alive where Element.moveBefore exists. Contested languages, a
missing plugin, and a throwing fence fall back to Original. The harness
exposes loadPluginApp(...).codeFenceRenderers.
Core callers. MarkdownPreview's code renderer
(markdown-code-fence-renderers.tsx), the pipeline that also resolves
messageDirective, hosts every claimed fence; the built-in inline-vis
plugin renders through it. BB's mermaid diagram stays a core special case
inside Original, because it renders on every Markdown surface and this slot
is scoped to messages.
Docs: frontend-renderer-slots.md, testing.md.
Audit before stabilizing.
- Collision model. Confirm that a contested language falling back to BB's code block is enough, or whether users need a per-language picker like the source/diff renderer pin.
- Streaming contract. Confirm
code/status/indexcover real generative-UI renderers (partial JSON/DSL parsing), and whether the rest of the info string should be exposed asmeta. - Scope. Confirm the slot should stay limited to assistant and
nested-agent Markdown, whether common languages (
json,ts) should be claimable, and whethermermaidshould be claimable or reserved for BB. - Identity. Confirm keying a fence by source offset holds for every
path that rewrites a message's text, and decide whether iframes must
survive the tail-to-settled move in browsers without
moveBefore. - Source visibility. Decide whether BB should offer its own "view source" control for claimed fences instead of leaving it to each plugin.
Kept experimental (2026-08-22). There are no consumers. Items 4 (a paged/windowed read at 10k threads) and 5 (the draft indicator gap) cannot be resolved without one, and both change the contract.
The read hook returns the sidebar's live thread view (PluginSidebarThread
DTOs with a host-resolved indicator, plus sections) from the host's own
query and cache. The action hook is @internal and scheduled for removal
(see Scheduled removals). Neither
doc covers three September 2026 additions:
experimental_lifecycles(active, archived, or both; active by default). With it,PluginSidebarThreadsState.experimental_archivedexposes archive loading/error state, pagination flags andfetchNextPage; it is null for active-only reads.PluginSidebarThreadsState.experimental_hosts, the current machines in host query order, including those with no threads. Older hosts may omit it.openNewThread({ hostId }), unprefixed by request, preselects a machine for a new environment.environmentIdtakes priority.
Docs: frontend-registration.md.
Audit before stabilizing.
- DTO scope. Confirm every field earns its place and that the copy stays
worth its maintenance over
ThreadListEntry.hasUnsubmittedDraftis deliberately absent (client-local composer state); confirm plugins do not need it.statusandruntimeStatusfreeze the domain enums into the contract the wayindicatordoes, under the same treat-unknown-as-fallback rule.hostis resolved to{ id, name }; confirm resolution belongs here rather than in a separate hosts hook, and that falling back to the id for an unknown host is the right failure. - Indicator coupling.
indicatorfreezes bb's precedence into the contract. Confirm new kinds can ship without breaking plugins, and that the documented "treat unknown as none" rule is enough. - Unread semantics.
isUnreadis plain read state, so it is true for child threads and running threads thatisUnreadDoneThreadexcludes by design. Confirm that is the more useful primitive for a replaced list. - Scale. Confirm one array of every thread is right at ten thousand threads, versus a paged or windowed read. Unchanged threads keep their DTO identity across refetches, but the host does not cap the array and plugin lists are expected to window their rows. Decide whether that expectation should be enforced by the contract before stabilizing.
- Draft indicators.
indicatornever reports "draft" or "working-draft", so an idle unread thread holding a draft reads as "unread-success" where the built-in row paints "draft". Decide whether to close that gap (a per-thread draft hook) or keep it documented. - Sections. Section writes are plain public API calls rather than
actions. Confirm that split holds once a replaced list ships section
drag-and-drop, where the built-in list's optimistic cache transactions have
no plugin equivalent.
openNewThread'ssectionId,environmentIdandhostIdride on router state; confirm that stays the right transport, and audit thehostIdselection semantics. experimental_useSidebarThreadPullRequest. A per-row, opt-in PR lookup sharing the host's environment-keyed query. It exposes normalizedexperimental_autoMerge,experimental_inMergeQueue(null means the queue lookup failed),experimental_checks,experimental_reviewandexperimental_mergeability. Confirm the state summaries and unknown-queue semantics meet sidebar needs, that a sidebar of many distinct worktrees does not stampede the git host, and that returningnullfor "lookup failed" is the right failure for a row that should simply show nothing.experimental_useSidebarThreadSplit. Gives a custom row the built-in drag-to-split gesture, with every layout rule owned by the host. Confirm a list with its own pointer-drag (reorder, swipe) still composes with the host's engage threshold; thatsplitPropsstaying an open object is the right forward-compatible shape, or whether it should narrow to a named handler; and that exposing the fullpanesarray does not leak more layout state than a row needs.- Archive selection and machine list. Audit archive-only
loading/errors, combined views, pagination retries, and archived row
actions. Audit empty machine sections, machine removal, rename updates, and
loading behavior for
experimental_hosts.
Kept experimental (2026-08-22). There are no consumers, and item 1 (merging
with bb.ui.registerThreadAction) is cheapest to decide before the first one.
Renders a plugin component in the thread header's action row, once per pane
with that pane's threadId, for controls that draw live state. A crash
removes only that pane's control.
Docs: frontend-registration.md.
Audit before stabilizing.
- Two APIs, one region.
bb.ui.registerThreadActionand this slot share a row. Confirm the ordering rule between them, and whether the two should merge behind one registration. - Budget. The row is short and already holds five host controls. Decide a cap, or an overflow behavior, before three plugins each add one.
- Compact viewport.
isCompactViewportasks every plugin to collapse itself. Confirm that beats a host-owned overflow menu. - Per-pane mounting. Confirm plugins handle mounting once per pane, and that a popover opened in one pane cannot leak into another.
- Height discipline. The host clamps the control's layout box
(
max-h-7 max-w-64) but deliberately does not clip overflow, because clipping also hides an anchored popover. A plugin can therefore still paint outside the row. Decide whether that trade is right, or whether the host should require a portal. - Other headers. Decide whether the compose screen, plugin panels, and the workspace header need the same slot, or stay host-only.
- Crash isolation. Each pane's mount has its own crashed-instance key, so one pane's crash does not disable another's copy; the thread-list slot deliberately does not, so a crash there disables it everywhere. Confirm that split, and decide whether other multi-mount slots need the same treatment.
app.slots.experimental_threadAction / experimental_useThreadActions / experimental_useThreadActionRegistrations / experimental_ThreadActionsMenu / experimental_ThreadActionsContextMenu / experimental_THREAD_ACTION_GROUPS (@get-bb/plugin-sdk/app)
A registration is { id, title, icon, group, order?, useData?, item }.
useData runs once per registration for the whole app and receives
{ threadIds }, every thread a visible surface evaluates actions for,
debounced (32 ms quiet, 100 ms max). item is a pure function of
{ thread, data, sdk, navigate } returning a PluginThreadAction or null;
throws drop the registration or that thread's item, and run errors are
contained. Registrations sort by group, order, then registration order
for every menu and the quick-action picker. Thread-list registers Move to
section, and push-notifications registers Notifications. The SDK test fake
throws when a menu trigger drops its props and ref.
Core callers. bb's own actions are CORE_THREAD_ACTIONS
(apps/app/src/lib/thread-actions/core-thread-actions.ts), registered under
the reserved bb--core owner. The header menu (ThreadDetailView →
ThreadDetailHeader), RootComposeMobileRecents, and
ThreadArchiveCommandHandler read the same registry, and the thread list's
row menus and quick actions use the SDK exports. The contract table in
ThreadActionsMenu.test.tsx runs every menu surface against the same
registrations.
Docs: frontend-registration.md.
Audit before stabilizing.
- Per-thread data.
threadIdsmakes each registration fetch and cache per-thread state itself. Decide whether a host attributes layer should batch those reads across registrations, and whetheruseDatashould move ontoexperimental_useRpcQuery. - Picker catalog. The row-actions picker lists every registration, including actions that rarely make sense as row buttons (Delete, New thread in environment). Decide whether a registration should opt out.
- Run input.
requestRenameis the only surface-supplied capability. Confirm no second one is needed before stabilizing the run signature. - Inline items.
inlineexists for surface-specific entries that are not about a thread. Confirm it should stay a component prop rather than a registration.
Returns (environmentId) => Promise<void>, core's environment-group archive:
the optimistic environments.archiveThreads, split-pane cleanup, route
repair, and one ten-second Undo toast that unarchives the returned ids
(lifecycle owners first) and returns to the route. Failures show a host error
toast, and archive failures reject. It replaces
PluginSidebarThreadActions.experimental_archiveEnvironmentThreads. The test
harness records calls in inspection.experimental_environmentArchiveCalls.
Core callers. The hook returns ThreadActionsProvider's
archiveEnvironmentThreads, bb's only environment-group archive, which the
deprecated sidebar-actions shim also calls. Its one caller is the built-in
thread list's environment group menu.
Docs: frontend-registration.md.
Audit before stabilizing.
- Scope. A per-flow hook was the smallest honest path. If more host UI flows need exposing (thread archive and delete outside a menu), decide whether they become thread actions, bound-SDK side effects, or a small flows area instead of one hook each.
- Undo route repair. The toast's route restore assumes the user has not navigated since; confirm that matches plugin-hosted surfaces.
- Coverage. Verify group archive and Undo with descendants, already archived threads, split panes, navigation during the Undo window, and failure rollback across sidebar organization modes.
Renders a plugin component beside the address bar in each Browser tab. It
receives threadId, tabId, url and a compact-viewport hint, with a crash
boundary per tab mount.
Docs: frontend-registration.md.
Audit before stabilizing. Confirm the toolbar can hold multiple plugin controls without crowding the address field, whether ordering needs a user preference, and whether plugins need browser instance or environment identity instead of resolving it server-side from the thread and tab ids.
Gives a Browser toolbar action script access to its tab's top-level document
without a CDP lease. evaluate(expression, { world }) runs in the main world
or bb's isolated world, where bb.postMessage reaches the calling plugin's
onMessage listeners. The value is null outside the desktop app.
Docs: frontend-registration.md.
Audit before stabilizing. Decide whether any enabled plugin may evaluate in
personal-profile tabs or whether this needs a user gesture, capability grant,
or origin allowlist. Plugins share one isolated world, so a plugin can post on
another plugin's channel; decide whether per-plugin worlds are required.
Confirm message size and rate bounds, subframe support, behavior during
navigation and renderer crashes, whether evaluate should time out while a
page is still loading, and whether an SDK or bb CLI surface is needed for
automation outside the toolbar component.
Lets a mention provider resolve a picked mention to agent-only image inputs, each optionally preceded by a short agent-only text input. The images are validated like ordinary prompt images before dispatch.
Docs: backend-ui-lifecycle.md.
Audit before stabilizing. Confirm images are the only binary input mention providers need, the 50-image boundary is appropriate, and local image access should remain governed by the thread dispatch validator rather than an earlier plugin-specific check.
Michael decided to ship these under final names as an explicit exception to
the experimental-prefix rule, so plugin authors migrate once.
useComposer() returns one stable handle per composer. removeMention,
onSubmitted, submit and setSelection replace their experimental_
names, along with ComposerCustomization.sendMenu,
PluginMessageActionContext.composer, and useComposers(). Replaced members
are @internal: stripped from published declarations but still exported and
implemented at runtime.
Docs: frontend-hooks-and-ui.md.
Audit.
- Handle semantics. A stable handle with reactive getters means memo
dependencies must name fields (
composer.draft), not the handle. Confirm the lint and documentation guidance is enough. - Mention shape.
ComposerMentionexposes core resource fields (path source and entry kind, command source and origin, and the stored path anattachmentmention shares with a file attached to the same draft). Confirm these are stable enough to be public. - Canonical pill text.
insertwrites the editor's canonical pill text (@labelfor plugin mentions), whileinsertMentionkeeps writing the bare label. Decide whetherinsertMentionshould converge. - Message-action composers. They have no slot lifecycle, so
setTextEffectandsetInputLockwarn and do nothing there, anduseComposers()handles behave the same way. Confirm. - Runtime-only aliases. Decide when, if ever, the runtime drops the
@internalnames. - Composer list order and membership.
useComposers()orders by mount and omits the sent-message editor. Decide whether panels also need the last-focused composer to pick a default target.
PluginComposerApi.removeMention({ provider, id }) removes the calling
plugin's matching mentions from the unsent draft.
PluginComposerApi.onSubmitted(listener) observes successful local
thread-send, queue-create and new-thread-create mutations in the composer's
scope. It is a local UI notification, not a server event.
Docs: frontend-hooks-and-ui.md.
Before stabilization, audit handoff scope routing, decide whether to include the submitted structured draft in notifications to distinguish annotations created while a request is pending, and verify disposal, failure restoration, mention rebasing, and callback failure isolation across every composer host.
submit({ sendAt?, experimental_data? }) submits the on-screen draft exactly
as pressing Enter would, and rejects with a user-presentable reason when it
cannot. experimental_data reaches every message.dispatch hook on the
initial attempt as an experimental_submission envelope carrying the
submitting plugin's id. Core validates the data as JSON but neither persists
nor interprets it.
Docs: frontend-hooks-and-ui.md, backend-events.md.
Audit before stabilizing.
- Programmatic send authority.
experimental_datapermits an immediate submission withoutsendAt. Confirm which composer customizations should receive that authority before stabilization. - Editors that save instead of send. The queued-message and sent-message
editors and the route-draft fallback reject with "cannot submit
programmatically".
isSubmittingBlockedlets a row disable itself before a click; confirm whether those surfaces also need a distinct capability flag. - Data visibility. Every dispatch hook sees the envelope and its owner id, not only the plugin that submitted it. Confirm that dispatch hooks remain the right trust boundary for plugin-owned submission data.
- Freshness of
sendAt. The host rejects a non-futuresendAtat call time, but the server accepts any non-negative timestamp and dispatches a past one at once. Decide whether the send/create routes should refuse a pastsendAtoutright. - No submission identity is returned. The method resolves with nothing, so a plugin cannot address the queued row it just created without listing the thread's queue. Confirm whether the queued message id belongs in the result.
- Plugin-hosted new-thread composers.
sendAtreaches a hosting plugin'sonSubmitand must be forwarded, but the submission envelope is not part ofNewThreadRequest, so another plugin'sexperimental_datacan be lost there. Decide whether to expose a forwardable experimental field or reject data-bearing submissions there.
setSelection(selection) sets a composer's pickers through the same handlers
the pickers call, so a plugin choice is indistinguishable from a hand-made
one. Fields the composer has no picker for are ignored. It resolves with the
composer's selection once values commit and the model catalog settles, or
after 15 s. PluginComposerApi.selection is the reactive read side. It is a
stable snapshot that re-renders consumers on picker changes, and it is null
for composers without pickers. Both are final-named under the composer API
exception.
Docs: frontend-hooks-and-ui.md (setSelection). No doc covers the
selection read.
Audit before stabilizing.
- Provenance. A value set by a plugin in the new-thread composer is
stored and reported exactly like a hand-picked one. Decide whether
executionInputSourcesshould carry a plugin source before a second consumer needs it. - Timeout shape. The bounded catalog wait resolves with the unsettled selection rather than rejecting. Confirm that is the right failure mode for a plugin that shows the result to the user.
- Provider inputs.
environment.inputsare not applied; the provider's inputs control keeps its own value. Decide whether the seed path's inputs handling should be reused here. - Guard semantics after a project switch. A call already in flight survives its own surface's unmount and resolves with the settled result. Confirm that is the behavior plugins expect, and whether the promise should also resolve early when the composer itself unmounts.
- Model-only thread changes. A model change in a thread without a provider change sets the next turn's model in place, as the picker does. Confirm plugins do not expect it to start a handoff.
- Empty selections in the result.
providerIdandmodelare omitted while nothing is selected andenvironmentwhile nothing is submittable, which overloads "missing" with "no picker here". Decide whether the result should distinguish them. - Selection read. Audit snapshot identity across provider catalog reconciliation and off-screen composer lifetimes.
bb.sdk.experimental_desktopBrowsers (ExperimentalDesktopBrowsersArea and
its ExperimentalDesktopBrowser* types) drives the desktop app's browser. It
provides explicit host/window/thread discovery, tab creation in the single bb
browser profile, and expiring control leases with scoped CDP connections. It
also provides capture, reveal, close and release, plus subscribe, which
polls tab state every two seconds and is not a lossless event log. Cookie
import from an installed browser uses listImportSources and importCookies.
Connection credentials stay private to workers on the browser host. The core
CLI is bb browser.
Before stabilization, audit control and cookie import authorization. Any caller with server access can control tabs carrying the user's logins and copy the desktop user's browser sessions into the bb profile. OS consent applies only where the platform demands it: macOS Keychain for Chromium, Full Disk Access for Safari, and none for Firefox or keyring-free Linux Chromium. Decide whether control or import should require a desktop-side confirmation. Also audit per-tab mutual exclusion and child-target scope, native popup handling, debugger detachment, daemon/desktop disconnect and reconnect generations, expiry and cancellation races, bounded screenshot bytes, and cross-platform desktop startup. Cloud browsers and external provider registration are outside this surface.
bb.sdk.hosts.get({ hostId }) includes nullable threadStorageRootPath from
the machine's latest daemon session, without waking the machine (CLI:
bb machine show). bb.sdk.environments.experimental_cleanup({ environmentId })
requests removal of an unused provider-managed environment, overriding
retention and bypassing cleanup backoff. It rejects live threads and unmanaged
environments and succeeds for already-removed ones. Acceptance is not
completion: read the environment lifecycle fields for progress (CLI:
bb environment cleanup).
Before stabilization, audit path portability and session freshness after machine re-enrollment, and cleanup retry behavior across unavailable providers, concurrent attempts, and environments shared by archived and live threads.
experimental_spawnPortableOutputProcess spawns an output-only child process,
and experimental_sanitizeInheritedChildProcessEnv
(ExperimentalSanitizeInheritedChildProcessEnvArgs) filters the inherited
environment, for host-local plugin commands.
Before stabilization, audit command cancellation, inherited environment filtering, and portable output handling for host-local plugin commands on every supported OS.
Says why a timeline row holds a preview instead of the full output, as
returned by bb.sdk.threads.timeline. available means a detail read can
still hydrate it. detail-limit means that read exceeded its byte budget.
retention-expired means the full value no longer exists.
Audit before stabilizing.
- Confirm these three states remain complete if another output storage tier or hydration limit is introduced.
- Confirm plugins need the storage-policy distinction rather than a simpler boolean indicating whether a detail read can succeed.
- Verify old persisted previews and mixed-version clients still receive a deterministic state before making the field stable.
The Markdown component accepts explicit { target, rootPath, threadId }
document context. target is a workspace or thread-storage live-file
identity, and rootPath is its resolved root. Relative links and images
resolve from the document's directory within that root. Thread-storage targets
must name the same thread. Malformed context never falls back to the ambient
workspace. Omitting it keeps message routing.
Stabilize after plugin consumers verify nested paths, source identity, missing files, containment and line locations.
Passes the owning file tab's latest one-based, inclusive
{ startLineNumber, endLineNumber } range to its opener (null for no
requested navigation). Each targeted open supplies a new object, so openers
observe its identity.
Docs: frontend-core-slots.md.
Audit before stabilizing.
- Verify first, changed, identical, and rapid targets across workspace, host, and thread-storage files, tab remounts, and server synchronization.
- Validate the identity-based repeat signal with third-party openers and decide whether an explicit request sequence is needed before stabilization.
- Confirm absent-target, inclusive selection, EOF clamping, column support, file-tree navigation, and unsaved-edit behavior with more editor consumers.
- Verify older hosts omit the prop safely and older plugins ignore it; audit reload restoration and cross-client range updates under the existing tab policy.
Sends a payload-less system config-changed notification to connected
clients, so configuration re-reads every registered provider's availability.
Docs: backend-machines.md.
Stabilization requires proving that a recheck from an unregistered or disposed plugin is inert, that a provider cannot use it to force repeated refreshes of unrelated configuration, and that pairing, unpairing and credential rejection each reach the Machines settings section without a manual reload.
Registers a server access provider: availability (optionally with a public
serverUrl), acquire({ key, hostId, signal }) returning a grant
{ id, serverUrl, headers? } or { status: "failed", message }, and
release({ key, hostId, grantId }). Machines attach the grant's headers to
enrollment and runtime traffic.
Docs: backend-machines.md.
Before stabilization, prove retry-safe acquire/release across process death, credential privacy and revocation, explicit and automatic defaults, expired codes, removal while a provider is unavailable, and both enrolment and runtime traffic with independent direct and Connect consumers. Availability does not prove reachability from a remote machine.
bb.experimental_machines.bootstrap prepares enrollment through the
provider's MachineExecutor and waits for the daemon connection. Each call
issues a fresh single-use credential for the same durable host identity. The
supporting MachineExecutorRequest, MachineExecutor,
MachineBootstrapRequest and MachineBootstrapApi belong to the experimental
PluginMachines, so their unprefixed names do not mean they are stable.
Docs: backend-machines.md.
Stabilization requires independent Modal and SSH consumers. It requires failure verification for expired credentials, concurrent retries, interrupted exchange, cancellation, identity mismatch, and restored snapshots. It requires an audit that credentials never enter resource data or logs. It must verify cleanup of a checkpointed allocation before successful enrollment, including a safe no-op uninstall when installation never began, and retry after partial installation. Migration and live vendor verification remain part of the integration release gate.
A required callback on PluginMachineProviderDefinition, used only when no
resource checkpoint exists. It discovers and removes uncertain allocations by
the durable launch key and must never allocate or bootstrap. Experimental
through bb.experimental_machines.
Docs: backend-machines.md.
Stabilization requires crash/abort coverage before submission, after submission but before checkpoint, eventual vendor discovery, and access-release retry coverage for each shipped provider.
An optional boolean on PluginMachineProviderDefinition, false by default,
for disposable compute. After the last live thread and its machine launch are
gone, core requests removal regardless of environment retirement policy.
Docs: backend-machines.md.
Stabilization requires another compute provider and recovery coverage across environment retirement, live-work races, provider unavailability, and repeated removal failures.
The provider context's projectCheckout.experimental_ownsPath is a required
boolean whenever a checkout is present. It is true only for a checkout core
materialised at that exact path, derived from persisted source ownership and
never from caller inputs. Core's environment hook policy uses it to apply to
fresh machine clones and leave user-maintained attachments alone.
Audit ownership propagation and recovery before stabilizing.
bb.sdk.hosts.experimental_suspend({ hostId }) drains active work (bounded at
five minutes) and invokes the provider's suspend callback. It rejects with
machine_busy while a live launch, provisioning environment or checkout
preparation ties work to the host. Plugins own idle timing and preservation.
Docs: backend-machines.md, backend-sdk.md.
Stabilization requires interruption, checkpoint/restart, removal serialization, failed drain, bounded drain and same-identity restore tests.
experimental_thread.events delivers { thread, sequence }, coalesced to one
notification per thread per second. experimental_terminal.input delivers
{ terminal } after real user input is forwarded. Neither exposes contents.
Docs: backend-events.md.
Stabilization requires coalescing tests, live streaming/terminal verification, current-status behavior, and review of event delivery overhead.
experimental_thread.parentChanged delivers { thread, previousParentThreadId } after a threads.update moves or releases a
thread, or after core releases an archived thread's unarchived children (one
event each, after the archive commits). Threads below the moved one get no
event. Push-notifications uses it to republish the moved subtree's levels.
Docs: backend-sdk.md.
Stabilization requires deciding whether a general thread.updated event
should replace it, and a second consumer.
experimental_host.deleted delivers the removed host's public DTO once,
fire-and-forget. A plugin not loaded at removal never sees it, so it must
also reconcile against a 404 from bb.sdk.hosts.get.
Docs: backend-events.md.
Stabilization requires deciding whether hosts deserve their own event map
instead of PluginThreadEventPayloads, covering removal paths added later, and
a second consumer.
experimental_environment.removed delivers { removal } (environment id,
pre-removal host and path, provider ownership, removal time) after a
provider's removal is committed. Delivery is ephemeral, and provider success
does not guarantee the path was deleted.
Docs: backend-events.md.
Stabilization requires review of the payload fields, repeat-removal semantics, notification timing, and recovery without guaranteed delivery.
getResource(hostId) returns core's persisted host resource JSON, or null when
the host or resource is absent. Any plugin can read it, following the host
access model. It makes no vendor observation, and callers parse the
provider-specific data.
Stabilization requires validating missing-resource semantics and use by additional machine providers.
bb.experimental_environments.register({ id, displayName, description, icon, machineProviderId, environmentProviderId })
declares a new-machine environment option backed by one concrete environment
provider. It supplies no lifecycle callbacks or inputs.
Docs: backend-machines.md, frontend-components.md.
Stabilization requires UI/CLI parity, registration validation, pre-allocation refusals, provisioning timeline coverage, clone-failure machine retention and later project-checkout/worktree reuse.
bb.sdk.hosts.experimental_create, experimental_getEnrollmentCommand,
experimental_listProviders, experimental_reconnect, experimental_suspend,
experimental_resume and experimental_retryCleanup have no unprefixed
aliases.
experimental_getEnrollmentCommand serves the in-memory manual setup command
and expiry only while that host is creating. Completion, removal, expiry and
server restart drop the command, which never enters persisted progress or
transcripts. experimental_reconnect({ hostId }) returns
{ command, windowsCommand, expiresAt, hostId }, expiring commands that
re-enroll an existing machine.
Docs: backend-machines.md, backend-sdk.md.
Before stabilizing, verify creation cancellation through host removal, same-host restoration, serialized removal, plugin callers and UI/CLI parity. The enrollment command requires expiry/removal, credential authorization and transcript-redaction coverage.
experimental_server.checkMove({ targetHostId, serverUrl }) returns the
pre-move checklist (ServerMoveCheckResponse).
startMove({ targetHostId, serverUrl, stopRunningWork: true, archiveExistingTargetServerData })
freezes this server, copies its data to the target, starts the new server
there, switches machines over, and retires this process. moveStatus()
reports the active and last move. A move whose activation was never confirmed
reports recovery_required and keeps this server up and frozen. In direct
mode the status carries destinationStatusUrl. cancelMove() works until the
switch starts, and in recovery_required it rolls the switch back.
export({ signal }) streams an unencrypted gzip archive with its sha256.
hosts.experimental_deleteOldServerCopy({ hostId }) deletes the locked old
copy. All of them refuse machine credentials. Everything except status and
cancel requires the default-off serverMove experiment (403
server_move_experiment_disabled). CLI:
bb server move|export|import|unlock|allow-connect|delete-old-copy|install-machine-service.
Before stabilization, audit: authorization for plugin backends (bb.sdk runs
with owner access, so a plugin can export every secret or move the server);
the switch ordering against the bb connect tunnel and daemons that miss
server.moved; archive size limits and streaming memory use; cancellation
and failure recovery at every step, including a server restart mid-move;
behavior when the target runs a provider-managed machine; and whether
startMove should return immediately or expose progress through a durable
operation id instead of the in-memory status.
app.experimental_icons.register({ name, component }) adds inline React
artwork to the shared app icon registry. Explicit registrations override
built-ins, and the first plugin id in lexical order wins collisions.
experimental_Icon renders a name with a fallback (default Zap).
Resolved explicit icon names win over plugin branding for per-item icons.
Docs: frontend-components.md.
Before stabilizing, audit component sizing and accessibility, fallback behavior, collision precedence, whether per-registration disposal is needed beyond the existing setup lifecycle, and compatibility for plugins vendoring older Icon components. Verify icon precedence and load/unload fallback across mentions, message actions, nav panels and fixed tabs, panel launchers and their tabs, and legacy sidebar-footer actions.
The shared frontend renderer for agent, machine and environment provider
artwork. It takes providerKind, provider and an optional fallback
(default Code), and resolves slot overrides, then the logo mask, the glyph,
and the fallback, without fetching.
Docs: frontend-components.md.
Stabilization: audit SDK prop ergonomics against all three provider kinds, same-id isolation and legacy override fallback, asset-vs-glyph precedence, cross-plugin overrides, reload/error/recursion behavior, accessibility and theme rendering on desktop and mobile. Keep metadata fetching and plugin branding separate from provider artwork resolution.
Renders bb's thread status glyph from PluginThreadStatusGlyphProps
(indicator, archived?, rowStatus, hideIdleDraftLabel, size); the
host owns icons, colors, and labels, and the caller resolves the indicator.
The SDK test fake exposes the props as data-* attributes.
Core callers. ThreadStatusGlyph
(apps/app/src/components/thread/ThreadStatusGlyph.tsx) is the SDK export
and the glyph in the thread search palette, the related threads info section,
and mobile recents. The thread-list plugin draws its rows and collapsed-group
rollups with it.
Docs: frontend-registration.md.
Audit before stabilizing.
- Indicator resolution. The caller resolves the indicator, so the
thread-list plugin keeps its own copy of the indicator precedence and labels
that
@bb/client-coreholds for bb's surfaces. Decide whether the SDK should export the resolver and labels instead. - Archived rows. No plugin passes
archivedyet; confirm the archived glyph belongs on this component rather than in the archived list.
Explicitly reconciles a provider-managed machine with core's recorded state.
For a suspended machine it starts the provider's suspension operation and
returns HTTP 202; callers poll host status for completion. Active and
transitional states are left unchanged. Provider suspend and resume must be
idempotent. CLI: bb machine reconcile.
Validate concurrent resume/removal, failure reporting, long-running caller behavior, and the scope of supported states before stabilizing this API.
bb.sdk.threads.spawn and bb.sdk.threads.fork accept
lifecycleOwnerThreadId, and thread responses expose its nullable value.
These are data fields on existing SDK methods, so they carry no prefix.
Audit before stabilization: immutable cross-project ownership, cross-host cleanup, archive/delete retries, creation races, and preservation of existing unowned threads.
PluginEnvironmentProviderDefinition.experimental_existingPath(inputs) returns
an absolute path or null from parsed inputs, without mutating anything. On an
existing machine, a usable environment of that project at the path is reused
with its provider, ownership, resource and cleanup identity intact. An
unusable one is refused, and a missing one goes through normal creation.
Other projects' managed paths remain forbidden.
Stabilization requires lifecycle coverage for reuse, missing paths, cleanup in progress, cross-project ownership, and concurrent creation before binding.
Michael explicitly requested the final names insert and replace, an
exception to the experimental-prefix rule. PluginComposerApi.replace takes
an explicit ComposerDraftReplacement or a synchronous updater from the
latest ComposerDraftSnapshot, and never infers mention reconciliation.
insert inserts at the cursor or end. The legacy setText, updateText,
clear, addQuote, insertMention and removeMention are @internal
runtime-only methods.
Docs: frontend-hooks-and-ui.md.
Before stabilization, audit snapshot identity and updater failure/lifetime behavior across mounted, offscreen, and ephemeral editors, replay of attachment paths in their owning project, and migration of third-party text transforms to explicit mention ranges. Decide whether command application merits a shared public operation.
PluginSidebarThreadActions.openNewThread accepts experimental_placement
({ sectionId: string | null, pinned: boolean }), which overrides the legacy
section option. Omitting it clears prior composer placement and uses the
legacy section or the general list, unpinned. The composer sends this
placement with normal and scheduled creation.
Audit pinned groups, custom sections, project/machine groups, route transitions, draft recovery, and third-party sidebar compatibility before stabilizing this option.
package.json → bb.experimental_providers statically declares
{ kind, id, displayName } provider identities before a plugin runs. Only
kind: "agent" is accepted. It keeps a disabled plugin's agents discoverable
in Settings → Providers, and grants no runtime capability. Add "environment"
or "machine" only together with a consumer.
Stabilize after validating first-install discovery, shared-plugin enablement, dynamic provider removal, plugin upgrades, and duplicate-ID ownership behavior with third-party providers.
list({ cursor?, limit?, signal? }) returns { entries, nextCursor }, every
accepted user prompt newest first, each with id, createdAt, input,
projectId, threadId, and scope ("project" for a prompt that started
a top-level thread, "thread" for a follow-up). limit is a digit string (default 100, max 1000).
A page can be short while nextCursor is set, because unparseable rows are
skipped. The same route backs bb prompt-history list.
Before stabilization, audit whether limit should be a number, whether the cursor format needs versioning, whether project or thread filters belong on this call rather than on projects.promptHistory and threads.promptHistory, and whether skipped rows should fill the page.
The message reference handed to messageAction runs, ThreadChat consumer
message actions, and prose selections carries the event sequence that recorded
the message. It is the msg value of a message link and the seq accepted by
sdk.threads.message and bb thread log --message, so a plugin can build or
resolve a message link without guessing. It equals sourceSeqEnd except for a
steer, which is recorded by its request and shown at its acceptance.
Stabilize once message links have shipped and the seq has stayed stable across
edit-and-rerun, forks and context clears, and decide whether sourceSeqEnd
should remain alongside it.
Starts a server-owned plugin update and returns its job at once;
experimental_updateJobs.list/get report queued/running phases and the
terminal update, rollback, or failure. Jobs survive client disconnects, stay
ten minutes after finishing, and live in memory only. applyUpdate keeps its
result contract by polling the job; raw callers without
Prefer: respond-async keep the synchronous response. Running updates cannot
be cancelled during activation or rollback.
Stabilization requires exercising reconnect/reload, concurrent deduplication, rollback delivery, missing jobs after restart, and CLI/SDK parity.
Posts one anonymous product event to POST /api/v1/system/telemetry/events,
which forwards it to usage telemetry under the install's anonymous id and
drops it when usage data sharing is off. The schema is a strict allow-list of
setup-guide and new-thread-submit events; anything else is a 400. Core
callers, all through recordTelemetryEvent in
apps/app/src/components/onboarding/onboarding-telemetry.ts: the setup guide
(OnboardingFlow) and NewThreadComposer's new_thread_submit outcomes.
Stabilize after deciding whether third-party plugins may emit product events at all and, if so, how they are namespaced and rate limited.
PluginBbSdk.system.experimental_cliCommand, experimental_installCliCommand, and experimental_removeCliCommand
experimental_cliCommand() reports whether bb is on PATH on the server's
computer: supported (false without an installed bb-app launcher),
installedPath, onPath, and shadowedBy (source npm, machine, or
other). Install links <dataDir>/bin/bb into a PATH folder (on Windows,
adds <dataDir>\bin to the user PATH) and refuses to replace another bb
(409 cli_command_blocked); install and remove return the new status, fail
with 409 cli_command_unsupported when unsupported, and 403 for machine
credentials. Core callers: Settings → General → Command line
(CliCommandSettingsSection) and bb settings cli-command.
Stabilize after deciding whether the command should be installable per
machine through the host daemon (enrolled machines get ~/.local/bin/bb from
install-machine.sh today), and after exercising real Windows and macOS
installs.
hosts.experimental_discoverRepos({ hostId }) lists up to 10 git
repositories under the machine's home directory with local activity in the
last 30 days, newest first, each with path, name, lastActivityAt,
originUrl, and the bound projectId or null; truncated reports an
exhausted three-second walk budget. The walk skips dot, build, and scratch
directories, linked worktrees, and submodules. Core callers: the setup guide
(OnboardingFlow) and bb project discover; it rides the
host.discover_repos daemon command.
Stabilize after deciding whether depth, recency window, and limit should be caller options, and after exercising slow or network-mounted home directories and Windows hosts.
experimental_runVcsProcess(command, args, operation, options) admits a git
or gh operation to the runtime's shared queue before invoking it. args are
the command's arguments; they classify git network subcommands (fetch,
pull, push, clone, ls-remote, submodule update, remote update).
Arguments without a network subcommand, including an empty list for pipelines,
admit to the local pool. The call holds the permit until the returned promise
settles; callers must await child close and termination cleanup. Calls nested
inside an admitted operation run under the outer permit without queueing again.
ExperimentalVcsProcessOptions accepts priority (interactive, status,
background), an optional cancellation signal, and slots for pipelines
that start multiple git children. Queue cancellation prevents spawning; callers
own cancellation of running children. Execution timeouts begin after admission.
The runtime keeps two disjoint pools. The local pool admits
clamp(os.availableParallelism() - 2, 4, 6) local git children, computed once
per runtime: background work can fill two fewer slots and status work one
fewer, so eight or more cores give six, five, and four. The
network pool admits at most four network git or gh children, with at most two
gh: background work can fill two slots and status work three. Waiters gain one
priority level every 15 seconds. FIFO applies within effective priority in each
pool: when the first waiter that is not held back by the GitHub lane needs more
slots than its pool has free, later waiters in that pool wait behind it, so
pipelines are not starved by single-slot work. A blocked GitHub waiter does not
block eligible network git work, and neither pool blocks the other. The budget
is shared across module copies in one Node runtime, including bundled plugins.
Separate host daemons and server processes own separate budgets; arbitrary
commands run by agents or terminals are outside this queue.
Core buffered git/gh reads, streaming status/diff reads, blob reads, shell pipelines, watcher setup, environment provisioning, and the GitHub plugin use this path. The CLI and SDK's workspace reads and mutations reach the same host handlers. Host commands give explicit user mutations and diff panels interactive priority, status reads status priority, and PR polling/watcher setup/provisioning background priority.
Before stabilization, audit cancellation and permit release on all supported
platforms, mixed plugin bundles sharing the singleton, aging under sustained
load, whether the CPU-scaled local capacity needs a host override and reflects
container CPU quotas on supported hosts, whether argument
classification should cover more network commands (for example git lfs), and
whether work that escapes an admitted operation's async context should regain
admission. Verify that every caller holds permits until its last admitted child
exits, including pipelines.