Shows every UI slot: sidebar section, header and toolbar buttons, a settings page and a status bar item.

Sample mod for the $.slot API. It adds:
+1 button),slots: N status item at the bottom of the sidebar, above Settings (click adds one),N words badge, a Pin / Unpin button, and a Pinned in this chat block. Pins are per chat.Not installed by default. Copy this folder to ~/.pawn/mods/ui-slots-demo and allow it in the review dialog; the dialog lists the sidebar, toolbar, settings page, status bar, command palette, keyboard shortcut and message permissions.
Docs: docs/agent/MODS.md.
hooks/register.js 88 lines1// Clicks counted across every slot
2let clicks = 0
3let greeting = 'hello'
4// Pinned messages, per chat: sessionId -> Set of message ids
5const pins = new Map()
6let shortcutError = ''
7
8function pinsFor(sessionId) {
9 if (!pins.has(sessionId)) pins.set(sessionId, new Set())
10 return pins.get(sessionId)
11}
12
13// Each $.slot call is written out literally: consent shows what the source names,
14// and a slot call that isn't in the source is refused at runtime.
15function draw($) {
16 const el = $.ui.resolve({})
17 $.slot.sidebar({
18 id: 'demo',
19 title: 'Slots demo',
20 tree: [
21 el.Text({ text: 'clicks: ' + clicks + ' · ' + greeting }),
22 el.Button({ id: 'bump', text: '+1' })
23 ]
24 })
25 $.slot.header({ id: 'hello', label: 'Hello', title: 'Say hello' })
26 $.slot.toolbar({ id: 'reset', label: 'Reset', title: 'Reset the click count' })
27 $.slot.settings({
28 id: 'prefs',
29 title: 'Slots demo',
30 tree: [
31 el.Text({ text: 'Greeting shown in the sidebar section' }),
32 el.Input({ id: 'greeting', placeholder: 'Greeting…', value: greeting })
33 ]
34 })
35 $.slot.statusbar({ id: 'count', text: 'slots: ' + clicks, title: 'Click to add one' })
36 $.slot.palette({ id: 'bump', title: 'Slots demo: add one', hint: 'clicks: ' + clicks })
37 // A clash with an app or mod shortcut throws; it never overrides silently.
38 try {
39 $.slot.shortcut({ id: 'bump', keys: 'Mod+Shift+U', title: 'Slots demo: add one' })
40 shortcutError = ''
41 } catch (err) {
42 shortcutError = String((err && err.message) || err)
43 }
44}
45
46export function register(on) {
47 on('session.start', async ($, e, next) => {
48 if (e.plugin !== $.plugin.name) return next(e)
49 draw($)
50 return next(e)
51 })
52
53 // Every slot click arrives as ui.press with { plugin, id, slot }.
54 on('ui.press', { plugin: 'ui-slots-demo' }, async ($, e, next) => {
55 if (e.id === 'hello') $.ui.toast('Hello from the ' + e.slot + ' slot')
56 if (e.id === 'bump' || e.id === 'count') clicks += 1
57 if (e.id === 'reset') clicks = 0
58 if (e.slot === 'message' && e.id === 'pin') {
59 const set = pinsFor(e.sessionId)
60 if (set.has(e.messageId)) set.delete(e.messageId)
61 else set.add(e.messageId)
62 }
63 draw($)
64 return next(e)
65 })
66
67 on('ui.input', { plugin: 'ui-slots-demo', id: 'greeting' }, async ($, e, next) => {
68 greeting = String(e.value || '') || 'hello'
69 draw($)
70 return next(e)
71 })
72
73 // Badges, actions and blocks under finished messages. Never sees a streaming reply.
74 on('message.render', async ($, e, next) => {
75 if (e.role !== 'assistant') return next(e)
76 const words = String(e.text || '').trim().split(/\s+/).filter(Boolean).length
77 const pinned = pinsFor(e.sessionId).has(e.messageId)
78 const props = {
79 badges: [...e.props.badges, { id: 'words', text: words + ' words', tone: 'info' }],
80 actions: [...e.props.actions, { id: 'pin', label: pinned ? 'Unpin' : 'Pin' }],
81 blocks: pinned
82 ? [...e.props.blocks, { id: 'pinned', tree: $.ui.resolve({}).Text({ text: 'Pinned in this chat' }) }]
83 : e.props.blocks
84 }
85 return next({ ...e, props })
86 })
87}
88