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: send → thread.send {mode: 'queue'}, steer → thread.send {mode: 'steer'}, queue → thread.queue.create, sendQueueHead → thread.queue.sendHead, stop → thread.stop, create → thread.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 |