bb.files and bb.files-ui

The headless bb.files plugin owns file data, and bb.files-ui owns the default file presentation.

Purpose

bb.files gives plugins one typed service for confined file operations on workspace, host, and thread-storage targets.

bb.files-ui requires that service. It supplies replaceable file openers, source views, diff views, and extension-specific previews.

A user can replace one presentation contract without replacing file access.

The bb.files plugin (data)

bb.files is headless because plugins beyond the default file UI consume its service.

It provides read, write, watch, and upload operations. It also keeps the file-area list, directory, move, remove, and preview operations.

The plugin declares no surfaces. Its data model has no renderer, component, layout, icon, or color contract.

Surfaces

ID kind replaceable props contract sketch notes
This plugin declares no surfaces.

A data-service replacement does not implement bb.files-ui presentation contracts.

Common file identity

All public contracts use an ambient-context-free target. The union prevents invalid combinations of nullable IDs.

export type FileTarget =
  | {
      kind: "workspace";
      environmentId: string;
      path: string;
    }
  | {
      kind: "host";
      hostId: string;
      path: string;
    }
  | {
      kind: "thread-storage";
      threadId: string;
      path: string;
    };

export type FileLocation =
  | { kind: "line"; line: number; column: number | null }
  | { kind: "range"; startLine: number; endLine: number };

export type FileOpenDisposition = "preview" | "external" | "download";

export interface FileOpenRequest {
  target: FileTarget;
  location?: FileLocation | null;
  disposition?: FileOpenDisposition;
}

Paths stay relative to their target root. The service rejects traversal, an absolute workspace path, and an invalid thread-storage path. A host target can use an absolute path only when the caller's declared host scope permits it.

Services

ID kind replaceable version default provider notes
bb.files single yes 1.x bb.files The service provides in-process server methods over a private host role.

Consumers declare a required, optional, or watched edge to bb.files. A required consumer restarts after a provider change. Optional and watched consumers stay live and observe availability.

bb.files service

The service keeps the old file-area operations. It adds watch and upload as first-class methods.

method signature summary behavior
read (FileReadRequest) => Promise<FileReadResult> Reads text or bytes and returns a revision.
write (FileWriteRequest) => Promise<FileWriteResult> Writes bytes with an optional revision guard.
list (FileListRequest) => Promise<FileListResult> Lists one directory or a recursive subtree.
mkdir (FileMkdirRequest) => Promise<FileMutationResult> Creates one directory and optional parents.
move (FileMoveRequest) => Promise<FileMutationResult> Moves one path within the permitted host scope.
remove (FileRemoveRequest) => Promise<FileMutationResult> Removes one file or a permitted directory tree.
watch (FileWatchRequest, listener) => Promise<Disposer> Sends ordered changes and an explicit loss event.
preview (FilePreviewRequest) => Promise<FilePreviewLease> Creates a scoped, short-lived preview URL.
upload (FileUploadRequest) => Promise<FileUploadResult> Consumes a staged upload and writes it to a file target.
import { defineService } from "@get-bb/plugin/server";

export const bbFiles = defineService<BbFilesService>("bb.files", "1.0");

export interface BbFilesService {
  read(request: FileReadRequest): Promise<FileReadResult>;
  write(request: FileWriteRequest): Promise<FileWriteResult>;
  list(request: FileListRequest): Promise<FileListResult>;
  mkdir(request: FileMkdirRequest): Promise<FileMutationResult>;
  move(request: FileMoveRequest): Promise<FileMutationResult>;
  remove(request: FileRemoveRequest): Promise<FileMutationResult>;
  watch(
    request: FileWatchRequest,
    listener: (event: FileWatchEvent) => void,
  ): Promise<Disposer>;
  preview(request: FilePreviewRequest): Promise<FilePreviewLease>;
  upload(request: FileUploadRequest): Promise<FileUploadResult>;
}

export interface FileReadRequest {
  target: FileTarget;
  encoding?: "utf8" | "bytes";
  offset?: number;
  length?: number;
  ifRevision?: string;
  signal?: AbortSignal;
}

export type FileReadResult =
  | {
      encoding: "utf8";
      content: string;
      size: number;
      mediaType: string | null;
      revision: string;
      modifiedAt: number;
    }
  | {
      encoding: "bytes";
      content: Uint8Array;
      size: number;
      mediaType: string | null;
      revision: string;
      modifiedAt: number;
    };

export interface FileWriteRequest {
  target: FileTarget;
  content: string | Uint8Array;
  encoding?: "utf8" | "bytes";
  create?: boolean;
  createParents?: boolean;
  expectedRevision?: string | null;
  signal?: AbortSignal;
}

export interface FileWriteResult {
  target: FileTarget;
  size: number;
  revision: string;
  created: boolean;
  modifiedAt: number;
}

export interface FileListRequest {
  target: FileTarget;
  recursive?: boolean;
  includeHidden?: boolean;
  maxDepth?: number;
  signal?: AbortSignal;
}

export interface FileEntry {
  target: FileTarget;
  name: string;
  kind: "file" | "directory" | "symlink";
  size: number | null;
  revision: string | null;
  modifiedAt: number;
}

export interface FileListResult {
  entries: FileEntry[];
  truncated: boolean;
}

export interface FileMkdirRequest {
  target: FileTarget;
  parents?: boolean;
  signal?: AbortSignal;
}

export interface FileMoveRequest {
  from: FileTarget;
  to: FileTarget;
  overwrite?: boolean;
  expectedRevision?: string;
  signal?: AbortSignal;
}

export interface FileRemoveRequest {
  target: FileTarget;
  recursive?: boolean;
  expectedRevision?: string;
  signal?: AbortSignal;
}

export interface FileMutationResult {
  target: FileTarget;
  revision: string | null;
}

expectedRevision: null requires an absent destination. An omitted guard permits last-writer-wins behavior. A mismatched guard returns a typed conflict error with the current revision.

Watch

export interface FileWatchRequest {
  targets: readonly FileTarget[];
  recursive?: boolean;
  debounceMs?: number;
  signal?: AbortSignal;
}

export type FileWatchEvent =
  | { kind: "ready"; sequence: number }
  | {
      kind: "created" | "changed" | "removed";
      target: FileTarget;
      sequence: number;
      revision: string | null;
    }
  | {
      kind: "moved";
      from: FileTarget;
      to: FileTarget;
      sequence: number;
      revision: string | null;
    }
  | { kind: "lost"; sequence: number; reason: "overflow" | "reconnect" };

export type Disposer = () => void | Promise<void>;

The service orders events within one subscription. A lost event tells the consumer to call read or list again. The service does not claim that it can replay missing file events.

The disposer and the request signal both stop the watch. Plugin unload also stops every watch that the factory created.

Preview and upload

export interface FilePreviewLease {
  id: string;
  url: string;
  mediaType: string | null;
  size: number;
  revision: string;
  expiresAt: number;
  release(): Promise<void>;
}

export interface FilePreviewRequest {
  target: FileTarget;
  mediaType?: string;
  maxBytes?: number;
  ttlMs?: number;
  signal?: AbortSignal;
}

export interface FileUploadRef {
  id: string;
  name: string;
  mediaType: string | null;
  size: number;
  sha256: string;
}

export interface FileUploadRequest {
  source: FileUploadRef;
  target: FileTarget;
  expectedRevision?: string | null;
  createParents?: boolean;
  signal?: AbortSignal;
}

export interface FileUploadResult extends FileWriteResult {
  sourceId: string;
  sha256: string;
}

The kernel HTTP port stages browser uploads and returns FileUploadRef. The bb.files service validates the digest and consumes the staged bytes once.

preview does not expose an unconfined host URL. The server streams the target through the kernel HTTP port and binds the lease to the requester.

Exports

The data plugin publishes its service token, shared data types, and app client.

module exports use
@bb/files/contracts bbFiles, FileTarget, and all request and result types Define service edges and exchange file data.
@bb/files/app useFiles, useFile, useFilePreview Use the data client and reactive file state.
export interface FilesAppClient {
  open(request: FileOpenRequest): boolean;
  read(request: FileReadRequest): Promise<FileReadResult>;
  watch(
    request: FileWatchRequest,
    listener: (event: FileWatchEvent) => void,
  ): Promise<Disposer>;
  preview(request: FilePreviewRequest): Promise<FilePreviewLease>;
  upload(request: FileUploadRequest): Promise<FileUploadResult>;
}

export interface UseFileState {
  status: "loading" | "ready" | "error";
  result: FileReadResult | null;
  error: Error | null;
  refresh(): void;
}

Host roles

The first-party server artifact requires the private bb.files.host role. This role keeps machine file access outside the server process.

ID kind replaceable server owner host artifact notes
bb.files.host single no bb.files bb.files Private. It binds each request to a resolved host and confined root.
interface BbFilesHostRole {
  read(input: HostFileRead): Promise<HostFileReadResult>;
  write(input: HostFileWrite): Promise<HostFileWriteResult>;
  list(input: HostFileList): Promise<HostFileListResult>;
  mkdir(input: HostFileMkdir): Promise<void>;
  move(input: HostFileMove): Promise<void>;
  remove(input: HostFileRemove): Promise<void>;
  createPreview(input: HostFilePreview): Promise<HostPreviewArtifact>;
  consumeUpload(input: HostFileUpload): Promise<HostFileWriteResult>;
  startWatch(input: HostFileWatch): Promise<{ watchId: string }>;
  stopWatch(input: { watchId: string }): Promise<void>;
}

interface BbFilesHostSignals {
  fileChanged: {
    watchId: string;
    event: FileWatchEvent;
  };
}

The server resolves FileTarget to a host and root before it calls this role. The host role receives no project or thread lookup authority.

The public service owns the role. A consumer declares only an edge to bb.files and does not declare the host role.

The bb.files-ui plugin

bb.files-ui supplies the default presentation for the headless file service.

It requires bb.files. It owns no file access, storage, watch, upload, or host transport behavior.

Surfaces

ID kind replaceable props contract sketch notes
bb.files-ui.opener keyed yes FileOpenerProps The key is a normalized extension such as pdf. The * key is the default claimant.
bb.files-ui.source single yes SourceSurfaceProps The winner receives the typed first-party Original.
bb.files-ui.diff single yes DiffSurfaceProps The winner receives the typed first-party Original.
bb.files-ui.preview keyed yes FilePreviewProps The key is a normalized extension. The * key handles unknown and absent extensions.

The default bb.files-ui implementation declares these presentation child surfaces because it renders them.

bb.files does not declare them.

The kernel stores one global winner for each single surface and each keyed extension.

A keyed claimant does not receive Original. The runtime uses the declared default claimant after a keyed claimant fails.

bb.files-ui.opener and bb.files-ui.preview remain keyed contracts. Plugins such as monaco and pdf-preview claim their supported keys.

The runtime derives each key from the lowercase extension without a leading dot. It uses * when a target has no extension.

bb.files-ui.opener

The opener receives a live target. It can read or watch the file through the current bb.files app client.

export interface FileOpenerProps {
  target: FileTarget;
  extension: string;
  location: FileLocation | null;
  disposition: FileOpenDisposition;
  files: FilesAppClient;
  renderPreview(overrides?: {
    location?: FileLocation | null;
    className?: string;
  }): ReactNode;
  close(): void;
}

renderPreview() mounts bb.files-ui.preview with the same extension and target. It replaces the old keyed opener Original prop.

This function cannot call bb.files-ui.opener again.

One plugin can claim more than one extension. Each key has an independent winner and default claimant.

{
  "claims": [
    { "surface": "bb.files-ui.opener", "key": "pdf" },
    { "surface": "bb.files-ui.opener", "key": "epub" }
  ]
}

bb.files-ui.source

The source surface receives normalized defaults. A replacement can wrap or replace the exact first-party component.

export type CodeOverflowMode = "scroll" | "wrap";

export interface SourceCodeLineRange {
  start: number;
  end: number;
}

export interface SourceRenderProps {
  content: string;
  path: string;
  overflow: CodeOverflowMode;
  highlightedLines: SourceCodeLineRange | null;
  className?: string;
}

export type SourceSurfaceProps = SourceRenderProps & {
  Original: ComponentType<SourceRenderProps>;
};

The owner sets overflow to scroll and highlightedLines to null before it mounts the winner.

Line numbers are one-based and inclusive.

bb.files-ui.diff

The diff surface accepts one normalized unified patch. Full file text supports context expansion when the caller has it.

export type DiffViewMode = "unified" | "split";

export interface DiffFileContent {
  path: string;
  content: string;
}

export interface DiffFullFileContents {
  old: DiffFileContent;
  new: DiffFileContent;
}

export interface DiffRenderProps {
  patch: string;
  path: string;
  view: DiffViewMode;
  overflow: CodeOverflowMode;
  showLineNumbers: boolean;
  fullFileContents: DiffFullFileContents | null;
  className?: string;
}

export type DiffSurfaceProps = DiffRenderProps & {
  Original: ComponentType<DiffRenderProps>;
};

The owner sets view to unified, overflow to scroll, and showLineNumbers to true.

The stable fullFileContents field replaces the old experimental field.

bb.files-ui.preview

The preview surface receives a short-lived lease instead of a raw host path. The default opener creates and releases the lease.

export interface FilePreviewProps {
  target: FileTarget;
  extension: string;
  location: FileLocation | null;
  lease: FilePreviewLease;
  className?: string;
}

The lease URL carries a scoped token. It expires at expiresAt. A release revokes it before that time.

A preview claimant must not cache the URL after unmount. The surface boundary releases owner-created leases during automatic cleanup.

Services

bb.files-ui declares no data service. It consumes one required service edge.

service edge range use
bb.files required ^1 Reads files, watches changes, creates preview leases, and consumes uploads.

The kernel starts bb.files-ui after it resolves bb.files.

A service winner change restarts bb.files-ui with a fresh handle. A replacement service can keep the same contract.

Exports

The UI plugin publishes its surface types and exact first-party components.

module exports use
@bb/files-ui/contracts All surface prop types Define file presentation claims.
@bb/files-ui/components DefaultFileOpener, DefaultFilePreview, DefaultSourceRenderer, DefaultDiffRenderer Import an exact first-party component.
export interface FileLinkProps
  extends Omit<ComponentPropsWithoutRef<"a">, "href" | "target"> {
  target: FileTarget;
  location?: FileLocation | null;
  disposition?: FileOpenDisposition;
}

Use <Surface id="bb.files-ui.source" ... /> or <Surface id="bb.files-ui.diff" ... /> to follow the user's winner.

Import a Default* component when a component needs the exact first-party presentation.

The bb.ui plugin exports the shared FileLink, SourceCode, and Diff facades.

Those facades mount the current bb.files-ui surface winners.

Example

This plugin adds an extension-specific PDF preview. It keeps the first-party opener and replaces only the resolved preview body.

// bb.plugin.jsonc
{
  "id": "acme.pdf-preview",
  "version": "2.0.0",
  "claims": [
    { "surface": "bb.files-ui.preview", "key": "pdf" }
  ],
  "requires": [
    { "service": "bb.files", "range": "^1" }
  ],
  "artifacts": {
    "app": "./dist/app.js"
  }
}
// src/app.tsx
import { definePlugin } from "@get-bb/plugin/app";
import type { FilePreviewProps } from "@bb/files-ui/contracts";

function PdfPreview({ lease, className }: FilePreviewProps) {
  return (
    <iframe
      className={className}
      src={lease.url}
      title="PDF preview"
      sandbox="allow-same-origin"
    />
  );
}

export default definePlugin((api) => {
  api.surfaces.provide(
    "bb.files-ui.preview",
    { key: "pdf", component: PdfPreview },
  );
});

The surface boundary releases the lease when the preview unmounts. If this claimant fails, the kernel mounts the first-party pdf claimant or the * claimant.

Covers

old item ID new contract/verb note
server.sdk.files bb.files service The named service replaces the broad SDK area.
server.sdk.files.read bb.files service: read() The request uses FileTarget.
server.sdk.files.write bb.files service: write() expectedRevision keeps the concurrency guard.
server.sdk.files.list bb.files service: list() The result uses typed FileEntry values.
server.sdk.files.listPaths bb.files service: list({ recursive: true }) One method replaces the duplicate path-list verb.
server.sdk.files.mkdir bb.files service: mkdir() The request can create parent directories.
server.sdk.files.move bb.files service: move() The service checks both target scopes.
server.sdk.files.remove bb.files service: remove() Recursive removal requires an explicit flag.
server.sdk.files.createPreview bb.files service: preview() The result is a scoped preview lease.
app.slots.fileOpener bb.files-ui.opener keyed surface Each normalized extension is a key.
app.slots.experimental_sourceCodeRenderer bb.files-ui.source single surface The global winner replaces the local slot pin.
app.slots.experimental_diffRenderer bb.files-ui.diff single surface The global winner replaces the local slot pin.
app.contracts.PluginFileOpenerRegistration bb.files-ui.opener claim plus api.surfaces.provide() Static claims replace registration metadata.
app.contracts.PluginSourceCodeRendererRegistration bb.files-ui.source claim plus api.surfaces.provide() The surface declaration owns the props type.
app.contracts.PluginDiffRendererRegistration bb.files-ui.diff claim plus api.surfaces.provide() The surface declaration owns the props type.
app.contracts.PluginSourceCodeRendererProps SourceSurfaceProps The stable type keeps the resolved values and Original.
app.contracts.PluginDiffRendererProps DiffSurfaceProps The stable type keeps the resolved values and Original.
app.contracts.PluginFileOpenerProps FileOpenerProps FileTarget replaces separate path and source fields.
app.contracts.PluginFileOpenerSource FileTarget A discriminated union replaces nullable identity fields.
app.contracts.SourceCodeProps SourceRenderProps The exact component and surface use one base type.
app.contracts.DiffProps DiffRenderProps The exact component and surface use one base type.
app.contracts.ExperimentalFileLinkProps FileLinkProps The type loses the experimental prefix.
app.contracts.BbNavigate.experimental_openFilePreview useFiles().open({ disposition: "preview" }) The bb.files-ui.opener winner handles the request.
app.contracts.BbNavigate.experimental_openFileExternally useFiles().open({ disposition: "external" }) The app client keeps host routing private.
app.replacement.sourceCodePreference bb.files-ui.source global winner The kernel migrates the old local pin.
app.replacement.diffPreference bb.files-ui.diff global winner The kernel migrates the old local pin.
app.replacement.fileOpenerPreference bb.files-ui.opener keyed global winners One winner exists for each extension key.
app.replacement.fileOpenerOverride dropped bb.files-ui.opener has one global winner per extension. Import an exact component when choice must stay fixed.
app.contracts.SourceCodeLineRange SourceCodeLineRange The stable type keeps inclusive one-based lines.
app.contracts.CodeOverflowMode CodeOverflowMode The values stay scroll and wrap.
app.contracts.DiffViewMode DiffViewMode The values stay unified and split.
app.contracts.ExperimentalDiffFileContent DiffFileContent The type loses the experimental prefix.
app.contracts.ExperimentalDiffFullFileContents DiffFullFileContents fullFileContents replaces the experimental field.
app.contracts.PluginMessageDirectiveOpenWorkspaceFile useFiles().open() The caller supplies a workspace FileTarget.
app.contracts.PluginFileOpenerSource.kind FileTarget.kind The stable discriminant keeps the three source kinds.
app.contracts.PluginFileOpenerSource.threadId FileTarget thread-storage variant: threadId The field is required only for thread storage.
app.contracts.PluginFileOpenerSource.environmentId FileTarget workspace variant: environmentId The field is required only for a workspace.
app.contracts.PluginFileOpenerSource.projectId dropped environmentId identifies the workspace. Use bb.workspace to resolve its project.
app.contracts.PluginFileOpenerSource.experimental_hostId FileTarget host variant: hostId The field becomes stable and required for host targets.
app.contracts.PluginFileOpenerProps.path FileOpenerProps.target.path The target contains the confined path.
app.contracts.PluginFileOpenerProps.source FileOpenerProps.target FileTarget replaces the old source object.
app.contracts.PluginFileOpenerProps.Original dropped A keyed surface has no Original. Use renderPreview() for bb.files-ui.preview.
app.contracts.SourceCodeProps.content SourceRenderProps.content The field stays required.
app.contracts.SourceCodeProps.path SourceRenderProps.path The field stays required.
app.contracts.SourceCodeProps.overflow SourceRenderProps.overflow The owner resolves the default before the surface call.
app.contracts.SourceCodeProps.highlightedLines SourceRenderProps.highlightedLines The owner resolves absent input to null.
app.contracts.SourceCodeProps.className SourceRenderProps.className The optional root class stays available.
app.contracts.DiffProps.patch DiffRenderProps.patch The field stays required.
app.contracts.DiffProps.path DiffRenderProps.path The field stays required.
app.contracts.DiffProps.view DiffRenderProps.view The owner resolves the default before the surface call.
app.contracts.DiffProps.overflow DiffRenderProps.overflow The owner resolves the default before the surface call.
app.contracts.DiffProps.showLineNumbers DiffRenderProps.showLineNumbers The owner resolves the default before the surface call.
app.contracts.DiffProps.experimental_fullFileContents DiffRenderProps.fullFileContents The field becomes stable.
app.contracts.DiffProps.className DiffRenderProps.className The optional root class stays available.
app.contracts.PluginSourceCodeRendererProps.content SourceSurfaceProps.content The field stays required.
app.contracts.PluginSourceCodeRendererProps.path SourceSurfaceProps.path The field stays required.
app.contracts.PluginSourceCodeRendererProps.overflow SourceSurfaceProps.overflow The field stays resolved and required.
app.contracts.PluginSourceCodeRendererProps.highlightedLines SourceSurfaceProps.highlightedLines The field stays resolved and nullable.
app.contracts.PluginSourceCodeRendererProps.Original SourceSurfaceProps.Original A single surface winner receives the default component.
app.contracts.PluginDiffRendererProps.patch DiffSurfaceProps.patch The field stays required.
app.contracts.PluginDiffRendererProps.path DiffSurfaceProps.path The field stays required.
app.contracts.PluginDiffRendererProps.view DiffSurfaceProps.view The field stays resolved and required.
app.contracts.PluginDiffRendererProps.overflow DiffSurfaceProps.overflow The field stays resolved and required.
app.contracts.PluginDiffRendererProps.showLineNumbers DiffSurfaceProps.showLineNumbers The field stays resolved and required.
app.contracts.PluginDiffRendererProps.experimental_fullFileContents DiffSurfaceProps.fullFileContents The field becomes stable and nullable.
app.contracts.PluginDiffRendererProps.Original DiffSurfaceProps.Original A single surface winner receives the default component.
app.contracts.PluginMessageDirectiveProps.openWorkspaceFile useFiles().open() A workspace target replaces the nullable helper.
app.contracts.PluginFileOpenerRegistration.id bb.files-ui.opener claim key The extension key and plugin ID identify the claimant.
app.contracts.PluginFileOpenerRegistration.title plugin manifest name and description The winner picker reads static plugin metadata.
app.contracts.PluginFileOpenerRegistration.extensions repeated bb.files-ui.opener keyed claims Each extension becomes one static claim.
app.contracts.PluginFileOpenerRegistration.component api.surfaces.provide("bb.files-ui.opener", { key }, component) Behavior moves to the app factory.
app.fileOpener.Original dropped A keyed surface has no Original. Use FileOpenerProps.renderPreview().
app.fileOpener.path FileOpenerProps.target.path The target contains the path.
app.fileOpener.source FileOpenerProps.target FileTarget provides the stable source identity.