Skip to content

Build the First Loop

The Quick Start used built-in modules. Here you’ll write one yourself in ten lines so the runtime stops being magic.

import { defineKitModule } from '@atheory-ai/kitsune-core'
export const auditModule = defineKitModule({
name: 'audit',
events: {
'note.saved': (event) => {
console.info('[audit]', {
when: new Date(event.timestamp).toISOString(),
what: event.type,
where: event.context?.surface,
what_entity: event.entity ?? event.context?.entity,
})
},
},
})

That’s the whole module. A name and a map of event handlers. Listening to '*' matches every event. Listening to a specific type matches that type.

Modules can also handle commands:

export const clipboardModule = defineKitModule({
name: 'clipboard',
commands: {
'clipboard.copy': async (command) => {
const text = String(command.payload?.text ?? '')
await navigator.clipboard.writeText(text)
return { copied: text.length }
},
},
})

Commands return a result. Events don’t.

import '@atheory-ai/kitsune-app'
import '@atheory-ai/kitsune-ui'
import { notificationModule } from '@atheory-ai/kitsune-ui'
import { auditModule, clipboardModule } from './modules.js'
customElements.whenDefined('kit-shell').then(() => {
const shell = document.querySelector('kit-shell')!
shell.modules = [auditModule, clipboardModule, notificationModule()]
})

The shell installs its modules on connectedCallback. Order doesn’t matter for events (all matching handlers fire); for commands, the last registered handler wins.

<kit-shell name="my-app">
<kit-boundary surface="quick-actions" feature="utilities">
<kit-button
meta-event="note.saved"
meta-command="clipboard.copy"
meta-prop-text="A modular frontend just copied this."
>
Save and copy
</kit-button>
</kit-boundary>
</kit-shell>

Click. Check the console: an audit line appears. Check the clipboard: the text is there. Add notificationModule() and a <kit-toast-region> and you can also tell the user what happened — without changing the button.

click reaches <kit-boundary>
→ boundary calls parseMetadata() on the clicked element
→ boundary asks the shell's runtime to:
runtime.emit({ type: 'note.saved', context: { surface: 'quick-actions', feature: 'utilities' } })
runtime.command({ type: 'clipboard.copy', payload: { text: '...' }, context: ... })
→ emit fans out to every event handler subscribed to 'note.saved' (and '*')
→ command finds the handler registered for 'clipboard.copy' and awaits its result
→ diagnostics fire for each step (the debug module logs them)

The button never imported the audit module. The audit module never imported the button. They speak through the runtime.

  • The Runtime Loop — the longer version of this story
  • Modules — patterns for production modules (services, lifecycle, provider tokens)
  • Build Quill — chapter 5 turns the loop above into a localStorage persistence module