5. Slot catalog

This section lists every slot a plugin can occupy or declare, plus the pane kinds, tab kinds, route contributions, client command ids, and pin targets that sit beside the slot tree. The mechanism (register options, owner props, arbitration) is §4. Status per row: built = exists in kernel-ui or in the Stage 2 slice (stage-2/03 §2); spec = 08 only; deleted = RECONCILIATION. Where 08 and stage-2/03 disagree, the row carries the stage-2/03 name and the 08 name sits in a note. Occupants marked "(spec)" inside a built row are 08's Stage 3 occupants; the slot exists, they do not yet.

Key grammars (08 §0; one rule for every keyed slot). <kind> is a core row kind, snake_case (command, file_change). <pluginId>/<kind> is an extension kind or a plugin-defined request (secrets/request). <providerId>:<kind> is a provider-scoped override of a core kind (rows only; the only place a provider id appears in a key). approval:<subject> is an interaction key over the closed set command | file_change | permission_grant | plan (R27, snake_case); question is the core question; a provider's tool-use prompt is the open kind <providerId>/tool_use, not a fifth subject. tool-ui:<pluginId>/<renderer> names a tool result's self-declared UI; tool:<name> names one tool. * is the fallback key. No globs, no reserved provider ids.

The timeline.row ladder has five rungs, passed as keys in this order (09 §2.6; stage-2/03 §2.5): tool-ui:<pluginId>/<renderer> (only kind === 'tool' with payload.result.ui) → tool:<payload.name> (only tool) → <providerId>:<kind><kind> (<pluginId>/<kind> for extension kinds) → * (the ui-default-rows painter). Original follows the same order. 07 §5.2's three-rung example is superseded.

Scopes: root (one instance), thread (per thread id), pane (per pane id); composer children are instanced per owner mount (composerId). Kinds: single, list, keyed, chain. A dash in "Key grammar" means the slot is not keyed.

5.1 Root and shell (08 A.1; stage-2/03 §2.1)

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
root single root {} kernel-ui, a priori (boot: 'critical', fallback: 'owner'; create-ui-kernel.ts) ui-shell ShellRoot (priority 0) built
shell.titleBar single root { sidebarOpen: boolean } ui-shell ui-shell TitleBarLayer (fallback) built
shell.sidebar keyed root app | settings | extensions, chosen by ui-shell/sidebar-for from the focused pane kind; default app { open; width; setOpen(open) } ui-shell app → ui-sidebar; settings → ui-settings, extensions → ui-extensions (spec; empty in the slice) built
shell.main single root {} ui-shell ui-shell LayoutHost → kernel LayoutRoot (fallback) built
shell.overlay list root {} ui-shell ui-shell DialogHost, SidebarTriggerOverlay, IframeDragGuard built
pane.dock single pane { dock: DockState; tabs: TabEntry[]; ops: DockHandle; host: 'inline' | 'workspace' } (no paneId; use usePane()) ui-shell, child of shell.main (08 A.1 says child of root) none in the slice → ui-shell fallback PlainDockList; ui-panel-shell (spec) built
layout.dropZones single root { panes: PaneRect[]; label; hoveredPaneId | null }; rendered only during a drag ui-shell, child of shell.main (08 A.1: child of root) none in the slice; ui-panel-shell DropZones (spec) built
provider.icon keyed root <providerId>, * { providerId; size } ui-shell (08 lists it under A.14) provider-claude-code claude-code; * → kit generic glyph; codex, pi, cursor, opencode, omp, grok, hermes (spec) built

5.2 Sidebar (08 A.2; stage-2/03 §2.2) — ui-sidebar, occupies shell.sidebar:app

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
sidebar.top list root { compact } ui-sidebar ui-sidebar HistoryControls (10) built
sidebar.primaryActions list root { compact; activeProjectId | null } ui-sidebar ui-thread-list NewThreadButton (10); SearchToggle (20, only when search/search is bound) built
sidebar.nav list root { compact; activePath; reorder: { order; setOrder(ids) }; hidden } ui-sidebar none in the slice; one row per ui-shell/pages page with nav (tasks, docs, github, automations) + ui-extensions ExtensionsRow 900 (spec) built
sidebar.nav.trailing keyed root <pageId> { pageId; compact } ui-sidebar none; tasks TaskCountBadge (spec) built
sidebar.body single root SidebarBodyOwner = { activeThreadId | null; activeProjectId | null; searchQuery; compact; open(target, { where: 'focused' | 'new-split' }) } ui-sidebar ui-thread-list ThreadList built
sidebar.footer list root { compact; closeOnMobile() } ui-sidebar ui-sidebar BugReportButton (80); ui-shell AttentionGlyph (90); ui-settings SettingsButton 10, connect RemoteAccessButton 30, ui-updates UpdatesBadge 100 (spec) built
sidebar.search single root { query; setQuery(q); onNavigate(target); close() } ui-sidebar ui-thread-list ThreadSearchPanel ("search unavailable" without search/search) built

5.3 Thread list (08 A.3; stage-2/03 §2.3) — ui-thread-list, occupies sidebar.body

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
threadList.groupBy keyed root project | sections | machine { threads: ThreadListEntry[]; projects; sections; collapsed: Record<string, string[]>; pinned: string[] } ui-thread-list ui-thread-list project, sections; machine lands with ui-machines (spec) built
threadList.row.leading list root ThreadRowOwner ui-thread-list ui-thread-list ActivityDot (10) built
threadList.row.trailing list root ThreadRowOwner ui-thread-list ui-thread-list ShortcutPill (20); SplitMiniMap waits for ui-shell/layout-drag consumers built
threadList.row.status chain root ThreadRowOwner ui-thread-list threads RuntimeStatusGlyph (fallback); delegation ChildStatusGlyph (spec) built
threadList.row.menu list root ThreadRowOwner & { close() } ui-thread-list threads RenameItem 10, PinItem 20, MarkReadItem 30, ArchiveItem 40, DeleteItem 900 (all threads/threads methods, R11); ui-shell OpenInSplitItem 50 built
threadList.project.menu list root { project: ProjectListEntry; close() } ui-thread-list threads RenameProjectItem (20); ui-projects ProjectSettingsItem 10, AddLocalPathItem 30, RemoveProjectItem 900 (spec) built
threadList.group.menu list root { group: { kind: 'environment'; environmentId } | { kind: 'machine'; hostId }; close() } ui-thread-list environments NewThreadHereItem 10, RenameEnvironmentItem 20, ArchiveWorktreeItem 900; ui-machines MachineSettingsItem 10 (spec) built
threadList.section.menu list root { section: SectionListEntry; close() } ui-thread-list threads RenameSectionItem 10, RemoveSectionItem 900 built
threadList.section.actions list root { compact; groupBy; sortBy } ui-thread-list ui-thread-list DisplayOptionsMenu 10, NewThreadButton 40; threads NewSectionButton 20; ui-projects NewProjectButton 30 (spec) built
threadList.section list root { groupBy; collapsed: string[] } ui-thread-list ui-thread-list PinnedSection (10), then the groupBy output built

ThreadRowOwner = { thread: ThreadListEntry; isActive; compact; depth }. Sort modes are the ui-thread-list/sort registry (A.14), not a slot.

5.4 Thread page (08 A.4; stage-2/03 §2.4) — ui-thread-page, pane kind thread

The thread.* slots are children of the thread pane kind (panes.register({ children }), built), not of a slot.

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
thread.header single pane ThreadOwner (+ usePane() for dockHost, isFocused, isMaximized) ui-thread-page ui-thread-page ThreadHeader (fallback; inline title edit → threads/threads.update) built
thread.header.badge list pane ThreadOwner ui-thread-page threads ForkOfPill; delegation ChildOfPill, side-chat SideChatPill (spec) built
thread.header.actions list pane ThreadOwner ui-thread-page ui-shell PaneControls (990); git GitActionSplitButton 20, ui-panel-shell DockToggle 900 (spec) built
thread.timeline single thread ThreadOwner & { searchRowId | null; variant: 'full' | 'compact' } ui-thread-page ui-timeline-view built
thread.composer single pane ThreadOwner & { scope: DraftScope } ui-thread-page ui-composer (mode: 'followUp') built
thread.banner list pane ThreadOwner ui-thread-page providers ProviderCliVersionBanner 10; threads ArchivedBanner 20; environments EnvironmentGoneBanner 30 built

ThreadOwner = { threadId; projectId; thread: Thread }, where Thread is the threads/threads.show result (stage-2/03 §2.4; 08 A.4 writes ThreadHead). There is no thread.chat slot: ThreadChat is a component exported by ui-thread-page that renders thread.timeline + thread.composer (09 §2.11).

5.5 Compose page and homepage (08 A.5; stage-2/03 §2.4) — ui-compose-page, pane kind compose

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
compose.composer single pane { projectId | null; seed: NewThreadSeed | null; scope: DraftScope } ui-compose-page ui-composer NewThreadComposer (mode: 'newThread', 09 §2.11b) built
compose.sections list pane { projectId | null } ui-compose-page none (kept for plugins) built
compose.empty single pane { hasProjects } ui-compose-page ui-compose-page EmptyWelcome (fallback) built
compose.recents single pane { projectId | null; limit } ui-compose-page ui-thread-list MobileRecents (compact only) built
compose.banner list pane { projectId | null } ui-compose-page providers ProviderCliVersionBanner built

Fork drafts, initialPrompt, reuseEnvironmentId, sectionId, and thread handoff arrive as the intent of navigation.open({ kind: 'compose' }, { intent }), never as location.state (08 A.5).

5.6 Plugin pages (08 A.6) — ui-shell PluginPageFrame, pane kind page

The page pane kind and its route are built (stage-2/03 §2.4). ui-shell/pages has no registrants in the slice, and the two header slots are 08 only.

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
page.header.center keyed pane <pageId> { pageId; subPath } ui-shell each page's breadcrumb / title cluster spec
page.header.actions list pane { pageId; subPath } ui-shell ui-panel-shell DockToggle (900); ui-shell PaneControls (990) spec

5.7 Panel shell (08 A.7) — ui-panel-shell, occupies pane.dock

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
panel.launcher list pane { threadId | null; projectId | null; open(tab: { kind; state }) } (+ usePane()) ui-panel-shell panel-files FilesSection (10); one ActionRow per tab kind with a non-null launcher; plugin panel actions (tasks, docs, github, workflows, side-chat) spec
panel.strip.actions list pane { dock: DockState; activeTabId } ui-panel-shell ui-panel-shell NewTabButton 10, CloseButton 20, CollapseConversationButton 30 (LayoutOps.dock.setMode) spec
threadInfo.row list thread ThreadOwner & { workspaceStatus } panel-thread-info threads ParentRow 10, ForksRow 20; environments EnvironmentRow 30, DirectoryRow 40; git BranchRow 50, MergeBaseRow 60, GitStatusRow 70; github PullRequestRow 80; threads ArchivedRow 90; git CommitsRow 100, ChangedFilesRow 110; panel-files ThreadStorageRow 120 spec

There is no panel.tab slot: the kernel mounts TabKind.Component per tab; a tab kind is replaced through kind arbitration and the tab:<kind> pin (08 A.7). In-panel splits are deleted (Q21).

5.8 Timeline (08 A.8; 09 §2; stage-2/03 §2.5) — ui-timeline-view, occupies thread.timeline

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
timeline.view single thread ThreadOwner & { rows: RowStore; frontier: Frontier } ui-timeline-view ui-timeline-view VirtualizedList (fallback) built
timeline.row keyed thread RowKey, five-rung ladder (preamble) RowRendererProps = { row; providerId; view: { depth; inClosedStep; dimmed; isFrontier; scopeActive }; expansion: { expanded; expandable; forced; toggle() }; children; Original } (09 §2.6) ui-timeline-view ui-default-rows for message, reasoning, command, file_change, tool, web_search, web_fetch, image_view, plan, compaction, task, interaction, turn, notice, and * (painter); provider-claude-code claude-code:task (Original = TaskRow); workflows task (spec) built
timeline.outline single thread ThreadOwner & { rows: RowStore } ui-thread-page ui-timeline-view Outline (hidden in compact) built
timeline.working single thread { frontier; thinking: string | null } ui-timeline-view ui-timeline-view WorkingIndicator (fallback) built
message.action list thread { row; selection: string | null; isLatestActionable }; the occupant ships MessageAction { id; label; icon; when(ctx); run(ctx) } (09 §2.10) ui-timeline-view ui-default-rows copy 10, fork 20, add-to-chat 40; timeline-edit-messages edit 30, side-chat reply-in-side-chat 50, send-to-main 60 (spec) built
interaction.renderer keyed thread approval:<subject>, question, <pluginId>/<name>; override <providerId>:approval:<subject> InteractionRendererProps = { interaction; view: 'takeover' | 'row'; resolve(value); interrupt(); Original } (09 §2.12) ui-timeline-view ui-default-rows approval:command, approval:file_change, approval:permission_grant, approval:plan, question; provider-claude-code claude-code:approval:plan; ask-user-question ask-user-question/question, secrets secrets/request, provider-acp provider-acp/mcp-approval (spec) built
markdown.extension list (data) root MarkdownExtension { id; order; remark; rehype; components; directives; codeBlocks; linkRouter }; the registered component never renders ui-markdown ui-markdown core 0, mermaid 500, katex 510; inline-vis, workflows, tasks, docs, ui-mentions (spec) built
diff.renderer single root the kit Diff facade's DiffProps (stage-2/03 §4) + Original; kernel validates z.looseObject({ path }) kernel-ui, a priori (kit.ts, boot: 'deferred', fallback: 'none'; 08 A.8 says ui-diff declares) ui-diff Diff built
source.renderer single root the kit SourceCode facade's SourceCodeProps + Original; kernel validates z.looseObject({ path, content }) kernel-ui, a priori (08 A.8 says ui-diff declares) ui-diff SourceCode built

One interaction.renderer slot serves the composer body chain (view: 'takeover') and the row body (view: 'row'). Per-kind policy (expandable, bundling concept, summary verbs, auto-expand, hidden, text) is data on ui-timeline-view/kinds, not a slot (stage-2/03 §2.5). ui-markdown/render is a service key, not a slot (stage-2/04 D16).

5.9 Composer (08 A.9 names; 09 §3.5 order; stage-2/03 §2.6) — ui-composer, occupies thread.composer and compose.composer

Sixteen slots are children of ui-composer's two occupant registrations; composer.pickerFooter is declared by ui-execution-picker. All are scope pane, instanced per owner mount (composerId).

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
composer.stack list (order) pane ComposerOwner & { view: ComposerView }; occupant registers { chrome: 'card' | 'bare'; showDuringTakeover } ui-composer declared and empty in the slice. 09 §3.5 order: ui-thread-cards ChildInteractions 100; workflows ActiveRuns 200, BackgroundCommands 250; ui-thread-cards PromptMode 300, Goal 350, Todos 400, ThreadContext 500; github PullRequest 520; git ChangedFiles 540; ui-thread-cards ModelFallback 600, QueuedMessages 700; provider-retry RetryCountdown 750; plugins 800+ (spec) built
composer.body chain (overlay: true) pane ComposerOwner (+ selected) ui-composer ui-composer EditorCard (fallback, stays mounted hidden); InteractionTakeover (select(view) = view.takeover) renders interaction.renderer with view: 'takeover' built
composer.header list pane ComposerOwner ui-composer ui-composer ZenToggle (regular layout only) built
composer.editor single pane EditorProps (09 §3.6) ui-composer ui-editor-tiptap (Q23) built
composer.plusMenu list (order) pane ComposerOwner & { insertText(text); insertMention(resource) } ui-composer ui-composer Attach 100; provider-claude-code Plan 300 (from composerActions); skills Skills 200, automations Automation 500, Plugin 600 (08 A.9: plugin-store; 09 §3.5: ui-extensions), plugins 800+ (spec) built
composer.footerStart list (order) pane ComposerOwner & { selection: ExecutionSelection } ui-composer ui-execution-picker ExecutionPicker 100 built
composer.actions list (order) pane ComposerOwner ui-composer none in the slice; ui-voice MicButton 100 (spec); max 3 inline, overflow popover, usage-ordered built
composer.primaryAction chain pane ComposerOwner & { submitMode; verb: Verb } ui-composer ui-composer SubmitStop (fallback); ui-voice Mic selects on compact + empty draft + not running (spec) built
composer.belowRow list (order) pane ComposerOwner & { selection; runTarget: RunTarget | null } ui-composer ui-run-target ProjectSelector 100 (new thread), RunTargetPicker 200; ui-execution-picker PermissionModePicker 300; ui-thread-cards ContextWindowIndicator 900 (spec) built
composer.pickerFooter list pane { threadId | null; selection: ExecutionSelection['value'] } ui-execution-picker threads Handoff 100 (follow-up only) built
composer.pill keyed pane resource.kind: thread, project, section, path, command, plugin:<providerId>, * { resource: MentionResource; remove() } ui-composer ui-composer * plain pill; ui-mentions kinds (spec) built
composer.menuRow keyed pane typeahead source kind, * { row; active } ui-composer ui-composer *; ui-mentions path, command; github/tasks/docs rows (spec) built
composer.keymap list (priority, data) pane { id; priority; when?(ctx); run(ctx, event): boolean } (09 §3.9) ui-composer ui-composer's nine rules (ime-guard 1000 … submit 100) built
composer.submit list (order, data) pane SubmitMiddleware { id; order; when(ctx); disabled(ctx); run(ctx, next) } (09 §3.8) ui-composer the chain below built
composer.typeahead.source list (data) pane TypeaheadSource { id; kind; triggers; queryClass; section; openOnBareTrigger; search; apply } (09 §3.7) ui-composer none in the slice (a bare @ is literal text); ui-mentions path, thread, project, section on @, command on /, one relay per threads/mentions provider (spec) built
composer.voice single pane ComposerOwner & { recording: RecordingState } ui-composer none (fallback: nothing); ui-voice RecordingBar (spec) built
composer.attachments single pane ComposerOwner & { attachments: Attachment[]; remove(id) } ui-composer ui-composer AttachmentPreview (fallback) built

ComposerOwner = { composerId; mode: 'newThread' | 'followUp' | 'queuedEdit' | 'sentEdit' | 'embedded'; scope: DraftScope; threadId | null; projectId | null; draft; selection: ExecutionSelection; submitMode: 'ready' | 'queue' | 'blocked'; disabledReason: { reason; code } | null; layout: 'regular' | 'compact' | 'zen'; compactCollapsed }.

composer.submit chain by order (09 §3.8; stage-2/03 §2.6, frozen): 100 attachmentsReady (ui-composer) · 200 blockedReasonGate (ui-composer) · 250 archivedReadOnly (threads; disabled → { reason: 'This thread is archived', code: 'archived' }) · 300 runTargetValid (ui-run-target) · 400 providerReady (ui-execution-picker) · 500 provider middlewares (when: ctx.selection.providerId === ownId; plan-mode entry/exit lives here) · 600 serialize · 700 dispatch · 800 drafts · 900 navigate (ui-composer). The primary action's disabled reason is the first non-null disabled(ctx) in this order. Verb → command: sendthread.send {mode: 'queue'}, steerthread.send {mode: 'steer'}, queuethread.queue.create, sendQueueHeadthread.queue.sendHead, stopthread.stop, createthread.create. The composer never sends mode: 'auto'.

Takeover: while view.takeover !== null, composer.stack renders only cards with showDuringTakeover: true and the interaction renders as the last stack item; the editor stays mounted hidden so the draft survives. The archived / environment-gone banners stay in thread.banner, not in the stack (08 A.4).

5.10 Settings (08 A.10) — ui-settings, pane kind settings, shell.sidebar:settings

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
settings.bucket list root { bucketId; compact }; registration meta { id, label, icon } ui-settings ui-settings General 10, Appearance 20; ui-keyboard Keyboard 30; ui-settings UsageLimits 40; panel-files Files 50; ui-machines Machines 60; ui-updates Updates 70; plugin-store Marketplaces 80; ui-settings Experiments 90, Community 100; ui-thread-list ArchivedThreads 110 spec
settings.general.item list root {} ui-settings ui-settings GeneralCard 10; panel-browser OpenLinksInApp 12; connect RewriteLocalhost 14; ui-execution-picker StreamerMode 16; skills CliSkillsInstall 20; ui-voice VoiceInput 30; telemetry OptOut 40; ui-default-rows Debug 900 spec
settings.appearance.item list root {} ui-settings ui-thread-list SidebarThreadListPin 10 (writes slot:sidebar.body); ui-diff CodeRendererPin 20 (writes slot:diff.renderer, slot:source.renderer); ui-settings ThemePicker 30, FaviconColor 40, LightDarkMode 50; ui-shell SplitDimming 60 spec
settings.files.item list root {} panel-files panel-files OpenTargets 10, FileOpenerPins 20 spec
settings.provider keyed root <providerId> { providerId; declaration; values; set(key, value) } ui-settings ui-settings DeclarativeForm from the provider's settingsSchema unless the provider registers a component; every page ends with providers CustomModels spec
settings.plugin keyed root <pluginId> { pluginId; manifest; status } ui-settings ui-settings DeclarativeForm for settings.define; custom pages from connect, memory, keep-awake, custom-instructions, provider-retry, github, workflows spec
settings.machine.item list root { hostId; host } ui-machines ui-machines MachineInformation 10, PermissionLimit 20; providers ProviderClis 30; ui-machines AutoUpdate 50, DangerZone 900. No Roots item (R17) spec
settings.project.item list root { projectId; project } ui-projects ui-projects ProjectSources 10 spec

5.11 Extensions hub (08 A.11) — ui-extensions, pane kind extensions, shell.sidebar:extensions

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
extensions.section list root { sectionId; subPath; navigate }; meta { id, label, icon, routes: string[] } ui-extensions plugin-store Plugins 10; skills Skills 20 spec
extensions.hero single root { sectionId } ui-extensions plugin-store BrowseHero spec

5.12 Dialogs, toasts, attention, overlays (08 A.12; stage-2/03 §3)

One slot (shell.overlay, A.1) and three services. Listed so the catalog is complete; the service shapes are stage-2/03 §3.

Name Kind Scope Key grammar Contract (abridged) Declared by Default occupant Status
ui-shell/dialogs service (app) open(Component, props, { size; dismissible }) → { result; close() }; confirm({ title; description; confirmLabel; destructive }) → Promise<boolean>; holds the kernel ModalPresence; mounts into shell.overlay through DialogHost; never inert on the app root ui-shell built
kernel-ui/toasts kernel service show({ tone; title; description; action }), dismiss(id); drained into the kit's sonner toast by kitToastSink (kit.ts) kernel-ui built
ui-shell/attention service (app) set(source, level: 'none' | 'info' | 'attention' | 'urgent', target: app | thread | pane | nav), clear(source), useAttention(target); feeds favicon dot, title prefix, sidebar.footer glyph, threadList.row.status ui-shell built
shell.overlay list slot root {}; persistent mounts only ui-shell DialogHost, SidebarTriggerOverlay, IframeDragGuard built

5.13 File openers and open targets (08 A.13) — panel-files

Slot Kind Scope Key grammar Owner props (abridged) Declared by Default occupant Status
files.opener keyed pane <ext> lowercase without dot; * { file: FileRef; readBytes(); url(); lineRange | null; Original } panel-files panel-files SourceView (*, via source.renderer), ImageView (png jpg jpeg gif webp svg); docs MarkdownEditor (md mdx markdown); pdf-preview PdfView (pdf); per-extension pin slot:files.opener:<ext> spec
files.link.menu list root { file: FileRef } panel-files panel-files OpenInPanel, OpenWith…, CopyPath, RevealInFileManager spec

FileRef = { source: 'workspace' | 'host' | 'thread-storage'; path; hostId | null; environmentId | null; threadId | null }.

5.14 Registries and chains that are not slots (08 A.14; stage-2/03 §3)

Service-container registries and data-carrying list slots. "Kind" names the mechanism; they appear in bb ui catalog under "registries".

Name Kind Scope Key grammar Shape (abridged) Owner Default entries Status
rows reducer module manifest key contributes.reducers server <kind>, <pluginId>/<kind> module exports reducers: Record<kind, Reducer>; RowOp = upsert | append | delete (R7) ui-default-rows (core kinds); the declaring plugin (extension kinds) core + reducer-defined kinds spec
tail state threads/annotations <writerPluginId>/<name> annotations.set(threadId, key, value); useAnnotations(threadId, { name }) threads threads/pin, threads/pinOrder, threads/readMark, threads/section, ui-thread-list/attention, kernel-ui/dock.tabs; <providerPluginId>/promptMode, providers/armedActions, workflows/active (spec) built
composer.submit chain data list slot (A.9) pane SubmitMiddleware ui-composer the A.9 chain built
composer.typeahead.source data list slot (A.9) pane TypeaheadSource ui-composer none in the slice built
composer.keymap data list slot (A.9) pane { id; priority; when; run } ui-composer nine rules built
composer.history ui-composer/history ArrowUp recall from threads/prompt-history (thread and project scope) ui-composer spec
threadList.sortBy ui-thread-list/sort { id; label; compare } ui-thread-list updated, created, alpha, none built
links router ui-shell/links (chain semantics) register({ id; priority; match(url, ctx) → LinkAction | null }), open(url); highest priority first, first non-null wins; LinkAction = open | openTab | external | rewrite ui-shell threads threadLinks 400, ui-shell external 0; panel-files localFileLinks, panel-browser inAppBrowser, connect localhostRewrite (spec) built
pages ui-shell/pages <pageId> register({ id; nav: { label; icon; order } | null; Component; accessory: Component | null }) ui-shell declared, no registrants in the slice; tasks, docs, github, automations (spec) built
sidebarFor ui-shell/sidebar-for <paneKind> register(paneKind, 'app' | 'settings' | 'extensions') ui-shell default app; ui-settings → settings, ui-extensions → extensions (spec) built
layout drag ui-shell/layout-drag useDragToSplit().begin(event, target, label) ui-shell declared, no consumers in the slice built
files.openTarget panel-files/open-targets (host role registry) { id; label; icon; kinds: ('file' | 'directory' | 'terminal')[]; detect(); launch(target) } panel-files default-app, file-manager, terminal, macOS editor adapters spec
icons @bb/ui registerIcons(pluginId, map) <pluginId>/<name> names merge into one namespace @bb/ui core 45 + extended 186 spec
theme.palette ui-settings/theme manifest contributes.themes: [{ id; name; css | override; codeTheme }]; bb theme list|show|set ui-settings default, nord, dracula, solarized, gruvbox, catppuccin spec
commands kernel-ui commands (07 §6) flat ids (below) commands.register(contribution); commands.handle(id, handler, { priority?, executor? }) kernel-ui the pane.* table below built

app.contentScript is named only in 13 D.1 (contentScripts.register → "kernel-ui, keep"); it is in neither 08 nor kernel-ui and is not listed above.

5.15 Pane kinds (07 §4.2; 08 A.1; stage-2/03 §2.4)

panes.register(PaneKindInput) — not a slot. As built (kernel-ui/src/layout/layout-store.ts): { kind; schema; priority; Component; title(s); icon; threadId(s); dock: { default(s); allowedTabKinds } | null; equals; intents; children }. The collector fills priority 0, icon null, equals deepEqual, intents null. children declares the pane component's child slots (how ui-thread-page declares thread.*). Several plugins may register one kind; the kernel orders them with the pane:<kind> pin and the winner receives Original. An unknown kind renders the kernel MissingKindPane and the node is kept. threadId(state) sets the thread:<id> scope key for the pane's subtree.

Kind State Owner Route(s) threadId dock sidebar intents Status
thread { threadId } ui-thread-page /projects/:projectId/threads/:threadId s.threadId { default: () => ({ open: <client kernel-ui/dockOpenDefault>, mode: 'split', tabs: [{ kind: 'thread-info' }], activeTabId }), allowedTabKinds: null } app { focusPrompt?: true; scrollToSeq?: number } built
compose { projectId | null } ui-compose-page /, /projects/:projectId/compose null { default, allowedTabKinds: ['browser', 'terminal', 'file', 'plugin-panel', 'plugin-page', 'new-tab'] } app { focusPrompt?; initialPrompt?; reuseEnvironmentId?; sectionId? } built
page { pluginId; pageId; subPath } ui-shell /plugins/:pluginId/:pageId/* null as compose app none built
settings ui-settings /settings, /settings/:bucket, /settings/providers/:providerId, /settings/plugins/:pluginId, /settings/machines/:hostId null null settings spec
extensions ui-extensions /extensions/* null null extensions spec
project-settings ui-projects /projects/:projectId/settings null null app spec
machine ui-machines none (/settings/machines/:hostId is a settings sub-route; the kind exists for bb layout open) null null app spec
auth-callback { status: 'success' | 'error' } connect /auth/callback null null null spec (07 §3.1; R19)

"Settings replaces the workspace" is not a pane-kind flag: the settings and extensions panes write the tab-scope preference ui-settings/lastAppPath and their back arrow calls navigation.openPath(lastAppPath) (08 A.1).

5.16 Tab kinds (07 §4.2; 08 A.7)

tabs.register(TabKindInput). As built: { kind; schema; priority; Component; label(s); icon (required); singleton; persist: 'thread' | 'tab' | (s) => …; launcher: { label; order; create() } | null; closable }; the collector fills priority 0, singleton false, launcher null, closable true and records persist: 'dynamic' for the function form. Hosting is the pane kind's dock.allowedTabKinds, not a field on the tab. persist: 'thread' tabs live in the kernel-ui/dock.tabs thread annotation (CAS through kernel/preferences.set expectedUpdatedAt, #29); persist: 'tab' tabs live in the tab-scoped tree. No tab kind is registered in the slice: the thread pane's default thread-info tab renders MissingKindPane inside the empty PlainDockList (stage-2/03 §2.4, accepted).

Kind Owner State (sketch) persist singleton launcher Hosted by Status
thread-info panel-thread-info {} thread true { label: 'Thread info', order: 40 } thread spec
git-diff panel-git-diff { environmentId } thread true { label: 'Changes', order: 30 } thread spec
terminal panel-terminal { terminalId; target: TerminalTarget | null } thread false { label: 'Terminal', order: 20 } thread, compose, page spec
browser panel-browser { url; title | null; environmentId | null } tab false { label: 'Browser', order: 10 } thread, compose, page spec
file panel-files { source: 'workspace' | 'host' | 'thread-storage'; path; environmentId; hostId; threadId; lineRange; pinned } (s) => s.pinned ? 'thread' : 'tab' false null thread, compose, page spec
new-tab ui-panel-shell {} tab true null all spec
plugin-panel ui-panel-shell { pluginId; actionId; params: Json; title } thread false null thread, compose, page spec
plugin-page ui-shell { pluginId; pageId; subPath } thread false null thread, compose, page spec

The legacy side-chat tab kind is dropped; side-chat registers a plugin-panel action. file replaces today's three preview kinds with one discriminated schema (08 A.7).

5.17 Route contributions (07 §3.1; stage-2/03 §1)

routes.register(RouteContribution). As built (kernel-ui/src/router/router.ts): { id: '<pluginId>/<name>'; path; pane: { kind; toState(params, url); toPath(state) }; title(state) }. path takes :name segments and a trailing *. Two contributions with the same path are a RouteCollision for the later plugin by sorted id; there is no route priority (a fork uses replaces, which removes the original's routes). The kernel has no routes of its own; an unmatched URL renders the kernel NoRoutePage. One URL shape per pane kind; the personal project is an ordinary project id (no /threads/:id shortcut). url.hash reaches toState and PaneProps.hash; location.state is not used (one-shot payloads are intent). Legacy redirects are dropped.

Path Pane kind Owner Status
/projects/:projectId/threads/:threadId thread ui-thread-page built
/ compose ui-compose-page (stage-2/01, stage-2/03 §2.4; stage-2/04 D16 names ui-shell as the / owner in the hello-slot RouteCollision example) built
/projects/:projectId/compose compose ui-compose-page built
/plugins/:pluginId/:pageId/* page ui-shell built
/settings, /settings/:bucket, /settings/providers/:providerId, /settings/plugins/:pluginId, /settings/machines/:hostId settings ui-settings spec
/extensions/* extensions ui-extensions spec
/projects/:projectId/settings project-settings ui-projects spec
/auth/callback auth-callback connect spec

5.18 Client command ids (07 §4.7, §6.1; 08 A.14; kernel-ui/src/layout/layout-commands.ts)

Id grammar. Ids are flat, open, and today's ids verbatim (thread.new, panel.toggle, composer.focus); there is no ui.<surface> prefix and no manifest claim. A family is <noun>.<verb>.* with family: { count }, expanded to .1 … .<count>; the handler receives index. Uniqueness is per (side, id): the first registration in composition order owns a local id, and a second registration of the same id is conflict for the later plugin (degraded). pane is not a reserved noun on the client side. bindable: false hides an id from Settings › Keyboard. One executor per id; further handle calls are before interceptors by priority desc.

Kernel-owned (registerLayoutCommands, built). Every row: when: { all: ['splitActive'], none: ['modalOpen'] } (07 §4.7 writes none: []; built wins), bindable, web and desktop, no menu entry, registered before any plugin runs.

Id Title Family Default shortcut Runs
pane.focus.* Focus pane N { count: 8 } web + mac Control+N; web other Mod+Shift+N; desktop Mod+N focus the Nth pane in tree order
pane.focus.previous Focus previous pane none focus the previous pane, wrapping
pane.focus.next Focus next pane none focus the next pane, wrapping
pane.close Close pane Mod+Shift+X layout.close(focusedPaneId)
pane.maximize.toggle Toggle maximized pane Mod+Shift+E layout.maximize(...) toggle

Kernel context keys (07 §6.1): modalOpen (from ModalPresence, not a DOM selector), editableFocus, layoutCompact, webSurface, desktopSurface, mobileSurface, macPlatform, splitActive (bound to "more than one pane"). Plugins define the rest with commands.context.define.

Plugin-owned ids (08 A.14; R10):

Id(s) Owner Notes Status
thread.open, pane.act ui-shell bindable: false; executors for hub.command from ui-shell/layout.open | act (bb thread open, bb thread pane). Not kernel commands (R10) built (stage-2/03 §3)
window.new ui-shell spec
thread.* (thread.new, thread.jump.*, …) ui-thread-list / threads thread.jump.* is a family spec
sidebar.toggle ui-sidebar spec
panel.toggle, panel.close, panel.newTab, panel.collapseConversation ui-panel-shell over LayoutOps.dock.* spec
composer.focus ui-composer spec
modelPicker.* ui-execution-picker spec
settings.* ui-settings spec
file.quickOpen, workspace.openPreferred panel-files spec
diff.toggle panel-git-diff spec
terminal.open panel-terminal spec
browser.* panel-browser spec
question.select.* ui-default-rows family spec

Bindings are the profile preference ui-keyboard/keybindings (ui-keyboard; bb keybindings list|set|reset). No command palette in v1 (Q2).

5.19 Pin targets (02 §3.4; 07 §5.4; 08 B.3; kernel-core/src/names.ts)

kernel/pins is one profile-scope map. Its key is the pin target and is identical to the CLI argument: bb composition pin <target> <pluginId>. As built: PinTarget = service:<id> | slot:<name>[:<key>] | pane:<kind> | tab:<kind>. There is no kernel.pin. prefix (09 §2.6's kernel.pin.slot:… spelling is superseded by 08 B.3 and the built key) and no provider:<id> target (RECONCILIATION #28; the default provider is the preference providers/default). Arbitration is replaces → pin → priority → sorted plugin id; a pin on a multi-contract service may name an instance (service:kernel/secrets.backend os-keychain).

Target form Arbitrates Example Writer UI
service:<id> service candidates service:ui-markdown/render
slot:<name> a single or chain slot slot:sidebar.body Settings › Appearance SidebarThreadListPin (spec)
slot:<name>:<key> one key of a keyed slot slot:timeline.row:task, slot:files.opener:pdf CodeRendererPin writes slot:diff.renderer, slot:source.renderer (spec)
pane:<kind> pane kind registrations (layout-store.ts paneKinds_) pane:thread
tab:<kind> tab kind registrations tab:file

Base-layer winners (08 B.3): slot:root, slot:shell.titleBar, slot:shell.main → ui-shell; slot:sidebar.body, slot:sidebar.search → ui-thread-list; slot:thread.timeline, slot:timeline.view → ui-timeline-view; slot:thread.composer, slot:compose.composer → ui-composer; slot:composer.editor → ui-editor-tiptap; slot:pane.dock, slot:layout.dropZones → ui-panel-shell (spec; ui-shell fallback in the slice); slot:timeline.row:task → workflows (spec; ui-default-rows in the slice); slot:files.opener:md → docs, slot:files.opener:pdf → pdf-preview (spec); slot:diff.renderer, slot:source.renderer → ui-diff; pane:<kind> and tab:<kind> → their owners. A crashed winner falls back to the next candidate and shows one toast.

5.20 Names that do not exist

Name Source Use instead Status
composer.interaction RECONCILIATION #27; 08 adopted 09 §2.12's name interaction.renderer (one slot, view: 'takeover' | 'row') deleted
timeline.message.actions #27 message.action deleted
ui-mentions/providers #13 the threads/mentions multi-contract deleted
provider:<id> pin target #28 preference providers/default deleted
approval:fileChange, approval:permissionGrant R27 approval:file_change, approval:permission_grant deleted
kernel-owned pane.act, thread.open R10 ui-shell's client commands; no kernel-ui/layout service method deleted
kernel-ui/dialogs, kernel-ui/attention 08 A.1 ui-shell/dialogs, ui-shell/attention deleted
threads/actions R11 threads/threads methods deleted
thread.chat slot 09 §2.11 ThreadChat component from ui-thread-page deleted
panel.tab slot 08 A.7 TabKind.Component + tab:<kind> pin deleted
side-chat tab kind 08 A.7 a plugin-panel action deleted
delegation, backgroundTask row kinds 08 A.8 task with payload.role deleted