Block Kit reference
Decisive Block Kit is a declarative UI vocabulary. You return a tree of blocks; Decisive renders them with its own design-system components — so your extension looks native, themes correctly (dark-default), and stays consistent as the design system evolves. Blocks are also machine-readable, so the AI can render, fill, and trigger them.
Source of truth: decisive-sdk/src/blocks.ts.
Where blocks appear
The same blocks render everywhere: an action's run result (chat / tool output),
a panel, a modal, and coding-agent output.
Return them from any handler via Result.blocks.
Blocks
| Block | Shape |
|---|---|
| Header | { type: 'header', text } |
| Section | { type: 'section', text?, fields?: Md[], accessory? } |
| Markdown | { type: 'markdown', text } |
| Input | { type: 'input', element } — see inputs |
| Actions | `{ type: 'actions', elements: (Button |
| Table | { type: 'table', columns: string[], rows: Md[][] } |
| Context | `{ type: 'context', elements: (Md |
| Image | { type: 'image', url, alt, title? } |
| Divider | { type: 'divider' } |
Md is a Markdown string, rendered with the same renderer chat uses. A
section.accessory is a Button, SelectInput, or Image.
return {
blocks: [
{ type: 'header', text: 'Weekly summary' },
{ type: 'context', elements: ['Generated from 42 conversations'] },
{ type: 'markdown', text: summary },
{ type: 'table', columns: ['Theme', 'Count'], rows: [['Billing', '12'], ['Onboarding', '8']] },
{ type: 'divider' },
{ type: 'actions', elements: [{ type: 'button', id: 'refresh', text: 'Refresh' }] }
]
};
Inputs
Inputs collect form state. Each needs a unique id; its value arrives in your
handler as event.inputs[id].
// text | textarea | number | date
{ type: 'input', element: { type: 'text', id: 'topic', label: 'Topic', placeholder: '…', required: true } }
// select | multiselect
{ type: 'input', element: {
type: 'select', id: 'tone', label: 'Tone',
options: [{ label: 'Casual', value: 'casual' }, { label: 'Formal', value: 'formal' }],
initialValue: 'casual'
}}
// toggle
{ type: 'input', element: { type: 'toggle', id: 'notify', label: 'Notify channel', initialValue: false } }
Buttons & the interaction loop
Buttons (and selects) emit actions. When pressed, Decisive calls your handler's
onAction with an ActionEvent; you return a new
Result.
async run(input, ctx) {
const draft = await ctx.ai.complete({ prompt: `Write about ${input.topic}` });
return {
blocks: [
{ type: 'markdown', text: draft },
{ type: 'actions', elements: [
{ type: 'button', id: 'post', text: 'Post to chat', style: 'primary' },
{ type: 'button', id: 'discard', text: 'Discard', style: 'danger',
confirm: { title: 'Discard?', text: 'This cannot be undone.', confirm: 'Discard' } }
]}
]
};
},
async onAction(event, ctx) {
if (event.id === 'post') {
await ctx.chat.send(String(event.state?.draft ?? ''));
return { toast: { text: 'Posted', style: 'success' } };
}
return {};
}
A handler's Result can:
blocks— render/replace output,updateView: { blocks }— patch the current view in place,openModal— open a modal,toast: { text, style? }— show an ephemeral notice,error— surface a failure.
Button options
{ type: 'button', id: 'save', text: 'Save', style?: 'primary' | 'default' | 'danger',
value?: Json, // arbitrary state → event.value
confirm?: { title, text, confirm, deny? } } // confirmation gate
Threading state
To carry data from the producing view to onAction, put it in a button's value
or in a modal's state. It comes back as event.value / event.state.
Modals
interface ModalView {
title: string;
blocks: Block[];
submit?: string; // submit button label
close?: string; // close button label
state?: Json; // private metadata → event.state on submit
}
Open one by returning { openModal } from a handler, or imperatively with
ctx.ui.openModal(view). Submissions and in-modal buttons route back to onAction.
When blocks aren't enough
Block Kit is the default and covers most UI. For genuinely custom interfaces, Decisive falls back to a sandboxed iframe (see EXTENSIONS.md §4.1); prefer blocks for consistency and so the AI can drive your UI.