Slots
A slot is a named extension point owned by the component that renders it. ctx.slots.register(options, Component) returns a disposer. A registration can declare child slots; disposing it removes those children too.
| Slot kind | Composition |
|---|---|
list | Multiple entries |
keyed | Entries selected by key, such as a page route |
single | One active occupant, with a product fallback where provided |
Admission
Any slot ending in .widget is a general widget host. Declaring cockpit.widget admits the widget to other widget zones, including zones supplied by third-party layouts. The user can move it without reinstalling the package.
Every other slot requires an exact declaration. A widget does not gain access to root, page, nav.item, nav.footer, settings.section or settings.missions. Declaring one settings group does not open other groups.
Registering in an undeclared or refused slot throws. The error identifies the requested surface and manifest slots.
Slot lifetime
Register a normal widget in cockpit.widget. Chat widget zones are declared only while the chat page is mounted; use placement to move a widget there. Do not assume every destination exists when your plugin first loads.
For layout authors, a name ending in .widget promises to accept general widgets. Use a different name for a region requiring a specialised component contract. Render the slots you declare using the platform Slot or SlotKeyed components.
Special surfaces
agents.view is keyed and requires exact admission. A registered view receives no props; the Agents shell retains its top bar, selector and widget zones. Use a stable key and release it when unloading.
nav.item is a list slot: one entry of the side menu. Register it with key set to your page's route (ctx.slots.register({ slot: 'nav.item', key: '/growth' }, GrowthNav), lowercase, starting with /). The key is what the user's menu refers to: with it, your entry can be moved, hidden, separated and sent to the bottom of the menu like the product's, and it comes back to its place when your plugin is switched off and on again. Without a key it shows at the end of the main list and cannot be moved or hidden. While the user customizes the menu, Timon draws the row's grip and eye itself and makes your component inert: paint a plain link (icon and label) and leave hiding to Timon.
nav.footer is a list slot at the foot of the navigation, under the page entries (and under the pages the user moved to the bottom of the menu) and above the settings gear, in the rail and in the mobile drawer. It holds a control that applies to the whole interface rather than to one page. Declare it by exact name; declaring nav.item does not open it. Nothing is painted while it is empty.
kanban.column is a list slot rendered before status columns. It is not a widget destination or a new task status. Supply your own header and body; the host supplies column geometry, not task-drop handling. Create tasks through an explicit authorised operation instead of suggesting that dropping onto a decorative column changes a task's status.
kanban.todo.badge and kanban.todo.detail are list slots on an idea of the TO DO notebook: a small read-only badge next to the idea (on its line in the TO DO widgets, its Kanban card and its detail), and a block in the detail dialog. Declare them by exact name in client.slots. The component receives no props: call useTodoItem() (badge) or useTodoDetail() (detail, returns { item, saved }: call saved() after your own save so the notebook re-reads the idea) from @timon/ui. item.links holds the tickets the idea is tied to elsewhere.
chat.document is a single slot: the card of an HTML document in a conversation. It exists from startup, so a replacement registers in its apply whether a document is on screen or not. The component receives no props: call useUiDocument() from @timon/ui, and render UiDocumentFrame inside your card to keep the sandbox, the bridge and the focus guard (HTML documents in a conversation). Requires apiVersion: 9.
dictation.engine is a keyed slot that exists from startup: a dictation engine registers there with the dictation option, its key being the engine's id. It requires kind: dictation, the dictation:engine permission and apiVersion: 14; any other kind is refused. Its settings go in settings.voice, a list slot painted in Settings → Interface → Voice dictation.
model.picker is a single slot: the model selector of the whole interface — the chat composer, the task form, the agent editor and every other model field paint its occupant. It exists from startup; Timon's own picker is the fallback and comes back when your plugin is removed. The component receives no props: call useModelPicker() from @timon/ui, which returns the value, onChange, the optional inherit entry, filter, footer, disabled, the className to put on your root, and mode (panel or popover). In a popover Timon keeps the trigger, the anchoring, the focus and the closing: paint the content only; onChange closes it, unless popover.keepOpenOnPick says the surface closes it itself. Read the models with useBackendCatalog(): on a surface that chooses for an agent or a project, it already leaves out a provider that agent or project could not run on, and useModelPicker().target names who it is for. Keep three rules, which Timon also enforces on onChange: an account that cannot run (isPickable false) is shown but not choosable, the inherit entry hands back null, and a value whose provider left the catalogue stays shown, selected and disabled. Requires apiVersion: 13; template: examples/plugin-model-picker.
providers.cli is a single slot: the AI tool cards of the first-run Your AIs step, of its "cannot start" screen and of Settings › AI models › Providers. It exists from startup; Timon's own cards are the fallback and come back when your plugin is removed. The component receives no props: call useProviderCliCards() from @timon/ui, which returns mode (onboarding, recovery or settings), the backends (null while loading), unavailable (the viewer may not see them), the install jobs with the installer's console lines, updateCheck, busy, disabled, and four actions: install(name), refresh(), checkUpdates() and signIn(name), which types the tool's sign-in command in Timon's terminal (never pressing Enter) or copies it when the page has no terminal, and answers terminal, copied or failed. Timon runs the installer and follows it; your plugin names a tool, never a command. Offer Install only for a tool marked installable, link its installDocs otherwise, offer nothing to install or update for a tool marked unsupportedVersion (a version Timon does not support is installed), and show no update while updateCheck is false. Requires apiVersion: 14; template: examples/plugin-provider-cards.
task.form is a single slot: the form that creates or edits a Kanban task, opened from the board, the Agents page and the agents grid. It exists from startup; Timon's own form is the fallback and comes back when your plugin is removed. The component receives no props: call useTaskForm() from @timon/ui, which returns mode (create or edit), the edited task (or null), defaultAgent, the agents the screen holds, submit(body) (creates or updates the task and answers its id, without closing), the attachments actions (list, upload, remove, url), finish() (the screen reloads its tasks and the form closes), close(), remove (deletes the edited task, confirmation included, and closes once it is gone; null where the screen offers no deletion) and titleId. Timon keeps the window around your form: the focus stays inside and goes back where it was, and Escape closes it (call preventDefault() on an Escape you handle yourself). Put titleId on your heading, and send back the fields you do not show as the task holds them, so saving never changes them. Requires apiVersion: 14; template: examples/plugin-task-form.
settings.connection.form is a keyed slot: your own form for one of your connection types, registered under the type id (<plugin>:<name>@<version>). It receives no props either: call useConnectionForm() from @timon/ui. The same form adds and edits a connection: its mode says which.