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.