Skip to content
kitn AI/UI

Composer

kai-composer

A rich text input that behaves like a textarea but carries atomic entity pills — skills, agents, plugins — inserted from / and @ menus. It emits a structured { doc, text, entities } for your backend to expand, and you set its value as a string or a full document.

  • Shadow DOM
  • Atomic entity pills
  • / and @ trigger menus
  • Keyword highlighting
  • Undo / redo
  • Structured doc model
  • Pre-populate via doc value

Register elements once, then set the trigger definitions as a JS property and listen for events:

<kai-composer id="composer" style="display:block; width:100%;"></kai-composer>
<script type="module">
import '@kitn.ai/ui/elements';
await customElements.whenDefined('kai-composer');
const composer = document.getElementById('composer');
// `/` inserts skills; `@` opens a sectioned menu of agents + plugins.
composer.triggers = [
{ char: '/', kind: 'skill', items: [
{ id: 'summarize', label: 'Summarize', description: 'Summarize the thread' },
]},
{ char: '@', kind: 'agent', items: [
{ id: 'code-reviewer', label: 'Code Reviewer', group: 'Agents' },
{ id: 'documents', label: 'Documents', kind: 'plugin', group: 'Plugins' },
]},
];
composer.addEventListener('kai-submit', (e) => {
// e.detail = { doc, text, entities } — references, not resolved content.
console.log(e.detail.text, e.detail.entities);
});
</script>
  • Set objects as propertiestriggers, highlights, kindIcons, and a document value are arrays/objects, so assign them in JavaScript, never as HTML attributes. Scalars (placeholder, disabled, loading, maxHeight, submitOnEnter) work as attributes.
  • The document is the source of truth. Each pill is an EntityRef{ kind, id, label, icon?, promptText?, data? }. The composer emits references, never resolved content; your backend expands /summarize or @code-reviewer before the model call, exactly like Claude Code and OpenCode do client-side.
  • text is a default flatten — each entity becomes its promptText ?? label. Pass your own serializer if you need a different wire format.
  • Per-kind decoration. Skills and agents render as light, sigil-led text (/summarize, @code-reviewer); plugins render as a richer chip, since a plugin bundles tools/skills/agents. Override the colors with the --kai-pill-skill / --kai-pill-agent CSS variables.

Set triggers to enable the menus. Type / for a skill or @ for an agent or plugin; each selection inserts an atomic pill you can delete whole with Backspace. Arrow onto a pill to select it as one unit.

Set value to a ComposerDoc — an array of text and entity segments — to seed pills programmatically. They render on mount and stay fully editable. kai-submit still emits the flattened text plus the structured doc and entities.

Set highlights to decorate matching words via the CSS Custom Highlight API. This is decoration only — the text stays plain and editable, and highlights never enter the document model.

PropertyTypeDefaultNotes
valueControlled value — string or a full ComposerDoc (set as JS property).
placeholderPlaceholder text shown when the composer is empty.
disabledfalseDisable the composer entirely (non-interactive).
loadingfalseShow a loading/streaming state and block submit.
maxHeight240Maximum height in px before the content scrolls. Default 240.
submitOnEntertrueWhether pressing Enter (without Shift) submits. Default true.
triggersTrigger definitions — set as a JS property.
highlightsKeyword highlight rules — set as a JS property.
kindIconsDefault icon per entity kind (kind → image URL/data-URI) for items without their own `icon`. Overrides the built-in agent/plugin glyphs. JS property.
EventDetailNotes
The composer lost focus.
An entity pill was inserted into the composer.
An entity pill was deleted from the composer.
The composer gained focus. `focus`/`blur` are NOT composed natively, so they don't escape the shadow root — these re-expose them on the host. (For `keydown`/`paste`/`focusin`/`focusout`, listen NATIVELY on `<kai-composer>`: they're composed and already cross the shadow boundary.)
The user submitted the composer (Enter or programmatic submit).
A trigger character was detected at the caret (e.g. `/` or `@`).
Record<string, never> The active trigger was dismissed (Escape, space, or outside click).
The content changed (fires on every input event).