Cross-plugin claims

A plugin can claim a surface that another plugin declares. The declaring plugin owns the surface contract and its place in the graph.

The claimant supplies one implementation. The kernel connects both plugins through the contract ID.

This page builds the cross-plugin claims from bb.vcs-ui. It also composes the replaceable bb.files-ui.diff surface.

Use this when

  • Add content to another plugin's UI. Claim its typed list surface.
  • Keep domain data separate from presentation. Put data in a headless plugin and claims in a UI plugin.
  • Follow the active surface graph. Let a claim become inactive when the declaring winner omits its child surface.
  • Compose a replaceable surface. Render the current global winner instead of importing one implementation.
  • Handle an owner change. Keep independent claims active when only one graph path changes.

What you build

You will build the app artifact for bb.vcs-ui. The plugin adds two list items:

  • A version-control changes panel in bb.thread-ui.sidePanels.
  • A head item in bb.layout.statusbar.

The changes panel renders file patches through bb.files-ui.diff. It follows the user's global diff choice.

The data stays in the headless bb.vcs service. The UI plugin requires that service.

Understand the owners

The kernel declares only bb.app.root. Active surface winners declare all other surfaces.

The default thread view declares bb.thread-ui.sidePanels. The active layout declares bb.layout.statusbar on a different graph path.

  • The thread view declares bb.thread-ui.sidePanels. bb.vcs-ui adds vcs-changes, which composes bb.files-ui.diff.
  • The root mounts the layout. The layout declares bb.layout.statusbar, and bb.vcs-ui adds vcs-head.

bb.vcs-ui does not own these two list surfaces. It must use the types and rules from their owners.

Contract IDs use dots. Examples include bb.thread-ui.sidePanels, bb.layout.statusbar, and bb.files-ui.diff.

The owner declares one contract kind: single, list, keyed, or chain. A claimant must follow that kind.

1. Declare the claims

Every claim is static. Add each supplied surface to claims in bb.plugin.jsonc.

// bb.plugin.jsonc
{
  "id": "bb.vcs-ui",
  "version": "2.0.0",
  "claims": [
    { "surface": "bb.thread-ui.sidePanels" },
    { "surface": "bb.layout.statusbar" }
  ],
  "requires": [
    { "service": "bb.vcs", "range": "^1" }
  ],
  "artifacts": {
    "app": "./dist/app.js"
  }
}

Do not add a claim for bb.files-ui.diff. bb.vcs-ui uses that surface but does not provide it.

The build generates contract.json. It validates each claim against the surface owner's contract.

The host can inspect the claims before it loads the app artifact. It can also show their state in the global picker.

2. Add the changes panel

Import the surface types from the owner plugin. The panel contract supplies the current thread context.

// src/app.tsx
import type {
  ThreadSidePanelContribution,
  ThreadSidePanelProps,
} from "@bb/thread-ui/contracts";

function VcsChangesPanel(_props: ThreadSidePanelProps) {
  return (
    <section aria-label="Version-control changes">
      <h2>Changes</h2>
      <p>Select a file to see its patch.</p>
    </section>
  );
}

const vcsChangesPanel: ThreadSidePanelContribution = {
  id: "vcs-changes",
  title: "Changes",
  icon: "ListTree",
  order: 30,
  placement: "thread",
  layout: "flush",
  component: VcsChangesPanel,
};

The id identifies this item inside the list surface. It is not a contract ID.

This small panel confirms that the foreign claim works. A complete panel reads changes and history from the required bb.vcs service.

The app artifact does not start Git itself.

3. Add the head status item

The layout plugin owns the status bar contract. Import its types before you create the list item.

import type {
  LayoutStatusItemProps,
  LayoutStatusItemRegistration,
} from "@bb/layout/contracts";

function VcsHeadIndicator(_props: LayoutStatusItemProps) {
  return <span title="Current version-control head">Current head</span>;
}

const vcsHeadStatus: LayoutStatusItemRegistration = {
  id: "vcs-head",
  title: "Current head",
  icon: "GitBranch",
  order: 20,
  component: VcsHeadIndicator,
};

This small item confirms that the status bar claim works. The complete item reads the repository for the current route.

It shows the current head and its ahead or behind counts. It returns null when the route has no repository.

It does not add version-control data to the layout contract.

Both claims use list surfaces. All valid list items can stay active, and no item joins the winner picker as a replacement.

The user can hide or reorder each list item through the controls that the surface owner provides.

4. Compose the active diff winner

Use Surface when your component must follow a replaceable surface. Pass the props that its owner contract defines.

import { Surface } from "@get-bb/plugin/app";
import type { VcsFileDiff } from "@bb/vcs/contracts";

function VcsPatch({ file }: { file: VcsFileDiff }) {
  return (
    <Surface
      id="bb.files-ui.diff"
      props={{
        patch: file.patch,
        path: file.path,
        view: "unified",
        overflow: "scroll",
        showLineNumbers: true,
        fullFileContents: file.fullFileContents,
      }}
    />
  );
}

This component does not import DefaultDiffRenderer. It asks the kernel to mount the current bb.files-ui.diff winner.

bb.files-ui.diff is a replaceable single surface. Its selected provider receives the typed Original component.

VcsPatch does not receive Original, because it consumes the surface. Only the active single surface provider receives it.

5. Supply both claims

The app artifact uses one factory from @get-bb/plugin/app. The typed api object supplies both list items.

import { definePlugin } from "@get-bb/plugin/app";

export default definePlugin((api) => {
  api.surfaces.add("bb.thread-ui.sidePanels", vcsChangesPanel);
  api.surfaces.add("bb.layout.statusbar", vcsHeadStatus);
});

The loader stages both calls as one plugin generation. A factory error leaves no partial set of claims.

The kernel removes both registrations when the plugin unloads. You do not need a cleanup function for these calls.

What happens when the declaring winner changes

A child surface exists only while the active parent winner declares it. The claimant stays installed when that child disappears.

For bb.vcs-ui, each owner path changes independently:

Change Changes panel Head item Plugin state
A new thread view keeps bb.thread-ui.sidePanels Remounts in the new view Stays active Active
A new thread view omits bb.thread-ui.sidePanels Becomes inactive Stays active Active
A new layout keeps bb.layout.statusbar Stays active Remounts in the new layout Active
A new layout omits bb.layout.statusbar Stays active Becomes inactive Active
The old declaring winner returns Its child claims become active again Follows its own path Active

The host does not treat an absent child surface as a plugin failure. It shows the static claim as inactive.

A replacement can keep an ecosystem compatible by declaring the same child contract ID. It can omit the child to make an explicit break.

The kernel rebuilds the affected surface subtree after a winner change. Components in that subtree mount again, so their local state can reset.

The bb.files-ui.diff winner has a separate lifecycle. A diff winner change remounts every composed diff with the new global winner.

That change does not restart bb.vcs-ui. A bb.vcs service winner change does restart it because bb.vcs is a required service edge.

Pitfalls

  • Do not declare a foreign surface in your surfaces block. Claim the contract that its owner already declares.
  • Do not add a claim for a surface that you only render through Surface.
  • Do not import an exact component when you must follow the user's winner.
  • Do not assume that a child surface always exists. Its active parent winner controls its lifetime.
  • Do not use requires for a UI owner. Service edges apply to services, not surface presence.
  • Do not use one missing graph path to disable unrelated claims.
  • Do not expect Original on a list claim. Only a single surface winner receives it.

See also

  • Guide 7, Surfaces and kinds — claim each of the four contract kinds.
  • Guide 8, Services — use requires, optional, and watched service edges.
  • Guide 16, Typed contracts — publish and consume surface contract types.
  • design/08-bb-vcs.md — read the bb.vcs, bb.vcs-ui, and bb.git contracts.
  • Design section 4, The surface graph — understand child surface ownership.