The views-demo hooks module

Sample mod for $.view custom views. It:
$.store (called from inside the view through pawn.call), a Toast button, and buttons that open the same info page as a tab above the chat or as a dialog,pawn.send messages in a view.message hook and replies with $.view.post.Not installed by default. Copy this folder to ~/.pawn/mods/views-demo and allow it in the review dialog; the dialog lists Show custom views (medium risk) and the store/UI permissions the view bridges to.
Docs: docs/agent/MODS.md.
hooks/register.js 79 lines1// A custom view is plain HTML plus one script, run in a sandboxed iframe.
2// Inside it, `pawn.call(method, ...args)` reaches only the bridge calls this file
3// names literally ($.ui.toast, $.store.get, $.store.set below), and
4// `pawn.send(data)` arrives here as a `view.message` event.
5
6const STYLE = `<style>
7 button { font: inherit; padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-color);
8 background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; }
9 button.primary { background: var(--primary); color: var(--on-primary); border-color: transparent; }
10 .row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
11 .big { font-size: 28px; font-weight: 600; color: var(--accent); }
12 .muted { color: var(--text-muted); font-size: 12px; }
13</style>`
14
15const PANEL_HTML = STYLE + `
16 <div class="muted">Saved with $.store, inside a sandboxed frame</div>
17 <div class="big" id="n">…</div>
18 <div class="row">
19 <button class="primary" id="inc">+1</button>
20 <button id="toast">Toast</button>
21 </div>
22 <div class="row">
23 <button id="tab">Open tab</button>
24 <button id="modal">Open dialog</button>
25 </div>
26 <div class="muted" id="msg"></div>`
27
28const PANEL_SCRIPT = `
29 const n = document.getElementById('n')
30 const show = (v) => { n.textContent = String(v) }
31 pawn.call('store.get', 'count').then((v) => show(v || 0))
32 document.getElementById('inc').addEventListener('click', async () => {
33 const v = ((await pawn.call('store.get', 'count')) || 0) + 1
34 await pawn.call('store.set', 'count', v)
35 show(v)
36 pawn.send({ type: 'count', value: v })
37 })
38 document.getElementById('toast').addEventListener('click', () => pawn.call('ui.toast', 'Hello from a mod view'))
39 document.getElementById('tab').addEventListener('click', () => pawn.send({ type: 'open', placement: 'tab' }))
40 document.getElementById('modal').addEventListener('click', () => pawn.send({ type: 'open', placement: 'modal' }))
41 pawn.onMessage((data) => { document.getElementById('msg').textContent = data && data.text ? data.text : '' })
42 // Not on the bridge list: refused even though the frame asks.
43 pawn.call('process.run', ['id']).catch((e) => console.log('refused:', e.message))
44`
45
46const INFO_HTML = STYLE + `
47 <h3 style="margin:0 0 6px">Views demo</h3>
48 <p class="muted">This page is a mod view. It has no network access, no access to Pawn's window,
49 and can only call the $ functions its mod names in source.</p>
50 <div class="row"><button class="primary" id="close">Close</button></div>`
51
52const INFO_SCRIPT = `document.getElementById('close').addEventListener('click', () => pawn.send({ type: 'close' }))`
53
54export function register(on) {
55 on('session.start', async ($, e, next) => {
56 if (e.plugin !== $.plugin.name) return next(e)
57 $.view.open({ id: 'counter', title: 'Views demo', placement: 'panel', html: PANEL_HTML, script: PANEL_SCRIPT, height: 190 })
58 return next(e)
59 })
60
61 on('view.message', { plugin: 'views-demo' }, async ($, e, next) => {
62 const d = e.data || {}
63 if (d.type === 'count') {
64 $.view.post({ id: 'counter', data: { text: 'mod saw count ' + d.value } })
65 }
66 if (d.type === 'open') {
67 const id = d.placement === 'modal' ? 'about-modal' : 'about-tab'
68 $.view.open({ id, title: 'About views', placement: d.placement, html: INFO_HTML, script: INFO_SCRIPT, height: 170 })
69 }
70 if (d.type === 'close') {
71 $.view.close({ id: e.id })
72 }
73 return next(e)
74 })
75}
76
77// Bridge calls the view uses must appear literally in source:
78// $.store.get $.store.set $.ui.toast
79