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. |