SLOPSHOPPER

ui-slots-demo

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

newtoast
A shopper browsing a rack in a slop shop
README

ui-slots-demo

Sample mod for the $.slot API. It adds:

  • a Slots demo section at the bottom of the left sidebar (click count and a +1 button),
  • a Hello button in the chat header (shows a toast),
  • a Reset button in the composer toolbar (hidden on the empty home screen),
  • a Slots demo page under Settings → Mods with a greeting field,
  • a slots: N status item at the bottom of the sidebar, above Settings (click adds one),
  • a Slots demo: add one entry in the command palette (group Mods),
  • a Mod+Shift+U keyboard shortcut that adds one (Cmd on macOS, Ctrl elsewhere); if that chord is taken, the mod logs the conflict instead,
  • under every finished assistant message: a 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.

Source 1 files
hooks/register.js 88 lines
1// 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