pantheon-opencode 1.1.1-beta.1 → 1.1.1-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui.js","names":[],"sources":[],"mappings":""}
@@ -3,30 +3,11 @@
3
3
  /** @jsxImportSource @opentui/solid */
4
4
 
5
5
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from '@opencode-ai/plugin/tui'
6
- import { createMemo, createSignal, createResource, For, Show } from 'solid-js'
6
+ import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
7
7
 
8
- async function readPantheonVersion(api: TuiPluginApi): Promise<string> {
9
- try {
10
- const worktree = ((api.state as any).path?.worktree ?? '') as string
11
- const filePath = worktree ? `${worktree}/package.json` : 'package.json'
12
- const result = await api.client.file.read({ query: { path: filePath } })
13
- const content = typeof result.content === 'string' ? result.content : String(result.content ?? '')
14
- const match = content.match(/"version":\s*"([^"]+)"/)
15
- if (match?.[1]) return match[1]
16
- } catch { /* fall through */ }
17
-
18
- try {
19
- const proc = (api as any).client?.process
20
- if (typeof proc?.exec === 'function') {
21
- const result = await proc.exec({ command: 'git', args: ['describe', '--tags', '--always'] })
22
- const stdout = (result.stdout ?? result.output ?? '') as string
23
- const tag = stdout.trim().replace(/^v/, '')
24
- if (tag) return tag
25
- }
26
- } catch { /* fall through */ }
8
+ /* ─── Constants ─────────────────────────────────────────── */
27
9
 
28
- return '1.0.1'
29
- }
10
+ const BAR_WIDTH = 20
30
11
 
31
12
  const COMMANDS = [
32
13
  { name: '/pantheon', desc: 'Council synthesis' },
@@ -59,16 +40,183 @@ const AGENTS = [
59
40
  { name: 'talos', tier: 'fast' as const, role: 'Hotfixes' },
60
41
  ] as const
61
42
 
43
+ /* ─── Helpers ───────────────────────────────────────────── */
44
+
62
45
  function safeNum(v: unknown): number {
63
46
  return typeof v === 'number' && Number.isFinite(v) ? v : 0
64
47
  }
65
48
 
66
- function View(props: { api: TuiPluginApi; sessionID: string; version: string }) {
49
+ function fmtInt(n: number): string {
50
+ return Intl.NumberFormat('en-US').format(Math.max(0, Math.round(n)))
51
+ }
52
+
53
+ function fmtCost(v: number): string {
54
+ if (v <= 0) return ''
55
+ if (v < 0.01) return '<$0.01'
56
+ return `$${v.toFixed(2)}`
57
+ }
58
+
59
+ function msgTokens(msg: any): number {
60
+ const t = msg?.tokens ?? msg?.info?.tokens ?? {}
61
+ return safeNum(t.input) + safeNum(t.output) + safeNum(t.reasoning)
62
+ + safeNum(t?.cache?.read) + safeNum(t?.cache?.write)
63
+ }
64
+
65
+ function msgCost(src: any): number {
66
+ for (const c of [src?.cost, src?.info?.cost, src?.usage?.cost]) {
67
+ if (typeof c === 'number' && c > 0) return c
68
+ }
69
+ return 0
70
+ }
71
+
72
+ function buildBar(pct: number): string {
73
+ const clamped = Math.max(0, Math.min(100, pct))
74
+ const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((clamped / 100) * BAR_WIDTH)))
75
+ return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
76
+ }
77
+
78
+ /* ─── Version Detection ────────────────────────────────────
79
+ * 100% dinâmico — zero fallback hardcoded.
80
+ * Se falhar, retorna null e a View omite a versão.
81
+ *
82
+ * Try order:
83
+ * 1. api.client.file.read package.json
84
+ * 2. git describe --tags
85
+ * 3. opencode --version
86
+ * 4. null */
87
+
88
+ async function detectVersion(api: TuiPluginApi): Promise<string | null> {
89
+ // Try 1: package.json do projeto (via worktree path)
90
+ try {
91
+ const wt = ((api.state as any).path?.worktree ?? '') as string
92
+ const fp = wt ? `${wt}/package.json` : 'package.json'
93
+ const result = await api.client.file.read({ query: { path: fp } })
94
+ const match = String(result?.content ?? '').match(/"version":\s*"([^"]+)"/)
95
+ if (match?.[1]) return match[1]
96
+ } catch { /* fall through */ }
97
+
98
+ // Try 2: git describe --tags
99
+ try {
100
+ const proc = (api as any).client?.process
101
+ if (typeof proc?.exec === 'function') {
102
+ const r = await proc.exec({ command: 'git', args: ['describe', '--tags', '--always'], timeoutMs: 3000 })
103
+ const stdout = (r.stdout ?? r.output ?? '') as string
104
+ const tag = stdout.trim().replace(/^v/, '').replace(/-\d+-g[0-9a-f]+$/, '')
105
+ if (tag && tag !== '5.0.0') return tag
106
+ }
107
+ } catch { /* fall through */ }
108
+
109
+ // Try 3: opencode --version
110
+ try {
111
+ const proc = (api as any).client?.process
112
+ if (typeof proc?.exec === 'function') {
113
+ const r = await proc.exec({ command: 'opencode', args: ['--version'], timeoutMs: 3000 })
114
+ const stdout = (r.stdout ?? r.output ?? '') as string
115
+ const ver = stdout.trim().replace(/^v/, '')
116
+ if (ver) return ver
117
+ }
118
+ } catch { /* fall through */ }
119
+
120
+ return null // sem fallback — versão não aparece
121
+ }
122
+
123
+ /* ─── Context Bar ──────────────────────────────────────────
124
+ * Inspired by streetturtle/opencode-better-sidebar (context-progress)
125
+ * Shows: progress bar, token usage / limit, session cost */
126
+
127
+ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
128
+ const messages = createMemo((): any[] => {
129
+ try { return (props.api.state.session as any).messages(props.sessionID) ?? [] }
130
+ catch { return [] }
131
+ })
132
+
133
+ const usage = createMemo(() => {
134
+ const msgs = messages()
135
+ const lastAssistant = [...msgs].reverse().find((m: any) => {
136
+ const role = m?.role ?? m?.info?.role
137
+ return role === 'assistant' && safeNum(m?.tokens?.output) > 0
138
+ })
139
+ if (!lastAssistant) return null
140
+
141
+ const tokens = msgTokens(lastAssistant)
142
+ const pid = lastAssistant?.providerID ?? lastAssistant?.info?.providerID
143
+ const mid = lastAssistant?.modelID ?? lastAssistant?.info?.modelID
144
+ const provider = pid
145
+ ? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
146
+ : null
147
+ const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
148
+ const limit = ctxLimit > 0 ? ctxLimit : 200000
149
+ return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
150
+ })
151
+
152
+ const totalCost = createMemo(() => {
153
+ const state = ((props.api.state as any).session)?.get?.(props.sessionID)
154
+ const fromState = msgCost(state)
155
+ if (fromState > 0) return fromState
156
+ return messages().reduce((sum: number, m: any) => sum + msgCost(m), 0)
157
+ })
158
+
159
+ const theme = () => props.api.theme.current
160
+
161
+ return (
162
+ <Show when={usage()}>
163
+ {(u) => {
164
+ const pct = u().percent
165
+ const c = theme()
166
+ const barColor = pct >= 90 ? c.error : pct >= 70 ? c.warning : c.accent
167
+ const costStr = fmtCost(totalCost())
168
+ return (
169
+ <box flexDirection="column">
170
+ <box flexDirection="row" gap={1}>
171
+ <text fg={c.text} attributes={{ bold: true }}>Context</text>
172
+ <text fg={barColor}>{buildBar(pct)}</text>
173
+ <text fg={barColor}>{`${pct}%`}</text>
174
+ </box>
175
+ <text fg={c.textMuted}>
176
+ {`${fmtInt(u().tokens)} / ${fmtInt(u().limit)} tok${costStr ? ` ${costStr}` : ''}`}
177
+ </text>
178
+ </box>
179
+ )
180
+ }}
181
+ </Show>
182
+ )
183
+ }
184
+
185
+ /* ─── Session Row (single session with live status indicator) ─ */
186
+
187
+ function SessionRow(props: { api: TuiPluginApi; session: any }) {
188
+ const theme = () => props.api.theme.current
189
+
190
+ const status = createMemo(() => {
191
+ try {
192
+ const s = (props.api.state as any).session?.status?.(props.session.id)
193
+ if (s?.type) return s.type as string
194
+ } catch { /* ignore */ }
195
+ return null
196
+ })
197
+
198
+ const statusIcon = createMemo(() => {
199
+ switch (status()) {
200
+ case 'busy': return '\u25cf ' // ● — running
201
+ case 'retry': return '\u26a0 ' // ⚠ — needs attention
202
+ default: return '' // idle/unknown — no icon
203
+ }
204
+ })
205
+
206
+ return (
207
+ <text fg={theme().textMuted}>
208
+ {`${statusIcon()}${props.session?.id?.slice(0, 8) ?? '????'}... ${props.session?.title?.slice(0, 26) ?? '(untitled)'}`}
209
+ </text>
210
+ )
211
+ }
212
+
213
+ /* ─── Main Sidebar View ──────────────────────────────────── */
214
+
215
+ function View(props: { api: TuiPluginApi; sessionID: string; version: string | null }) {
67
216
  const [showCommands, setShowCommands] = createSignal(false)
68
217
  const [showAgents, setShowAgents] = createSignal(false)
69
218
  const [showConfig, setShowConfig] = createSignal(false)
70
- const [showMemory, setShowMemory] = createSignal(false)
71
- const [showSubagents, setShowSubagents] = createSignal(false)
219
+ const [showSessions, setShowSessions] = createSignal(false)
72
220
 
73
221
  const theme = () => props.api.theme.current
74
222
 
@@ -76,166 +224,208 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
76
224
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
77
225
  )
78
226
 
79
- const configInfo = createMemo(() => {
227
+ // ── Sessions: fetch via api.client.session.list (not proc.exec) ──
228
+ const [sessionList, { refetch: refetchSessions }] = createResource(async () => {
229
+ try {
230
+ const result = await (props.api.client as any)?.session?.list?.({ limit: 100 })
231
+ return result ?? { data: [] }
232
+ } catch {
233
+ return { data: [] }
234
+ }
235
+ })
236
+
237
+ // Total top-level sessions (excludes subagent sessions)
238
+ const totalSessions = createMemo(() => {
239
+ const result = sessionList()
240
+ if (!result) return 0
241
+ const data: any[] = (result as any).data ?? result
242
+ if (!Array.isArray(data)) return 0
243
+ return data.filter((s: any) => !s.parentID).length
244
+ })
245
+
246
+ // Recent top-level sessions, sorted by time.updated descending
247
+ const recentSessions = createMemo(() => {
248
+ const result = sessionList()
249
+ if (!result) return []
250
+ const data: any[] = (result as any).data ?? result
251
+ if (!Array.isArray(data)) return []
252
+ return data
253
+ .filter((s: any) => !s.parentID && s.id !== props.sessionID)
254
+ .sort((a: any, b: any) => {
255
+ const ta = a.time?.updated ?? a.updated ?? 0
256
+ const tb = b.time?.updated ?? b.updated ?? 0
257
+ return tb - ta
258
+ })
259
+ .slice(0, 8)
260
+ })
261
+
262
+ // ── Memory: starts at null (= "N/A"), tracks via events ──
263
+ const [memoryCount, setMemoryCount] = createSignal<number | null>(null)
264
+
265
+ // ── Event subscriptions for live session/memory updates ──
266
+ onMount(() => {
267
+ const cleanup: (() => void)[] = []
268
+
269
+ // Session events — triggers resource refetch
270
+ try {
271
+ cleanup.push(props.api.event.on('session.status', refetchSessions))
272
+ cleanup.push(props.api.event.on('session.created', refetchSessions))
273
+ cleanup.push(props.api.event.on('session.updated', refetchSessions))
274
+ cleanup.push(props.api.event.on('session.deleted', refetchSessions))
275
+ } catch { /* events API not available in this runtime */ }
276
+
277
+ // Memory events — tracks running counter (no api.state.memory)
278
+ try {
279
+ const ev = (props.api.event as any)
280
+ if (typeof ev?.on === 'function') {
281
+ cleanup.push(
282
+ ev.on('memory.created', () => setMemoryCount((c) => (c !== null ? c + 1 : 1))),
283
+ )
284
+ cleanup.push(
285
+ ev.on('memory.deleted', () => setMemoryCount((c) => (c !== null ? Math.max(0, c - 1) : 0))),
286
+ )
287
+ }
288
+ } catch { /* memory events not available */ }
289
+
290
+ onCleanup(() => cleanup.forEach((fn) => fn()))
291
+ })
292
+
293
+ // ── Config summary ──
294
+ const configSummary = createMemo(() => {
80
295
  const cfg = (props.api.state as any).config
81
296
  if (!cfg) return null
82
297
  return {
83
- plugins: Array.isArray(cfg.plugin) ? cfg.plugin : [],
84
298
  mcpCount: cfg.mcp ? Object.keys(cfg.mcp).length : 0,
299
+ pluginCount: Array.isArray(cfg.plugin) ? cfg.plugin.length : 0,
85
300
  autoCompaction: cfg.compaction?.auto === true,
86
301
  }
87
302
  })
88
303
 
89
- const memoryInfo = createMemo(() => {
90
- const mem = (props.api.state as any).memory
91
- if (mem) {
92
- const entries = safeNum(mem?.entries) || safeNum(mem?.count)
93
- return entries > 0 ? { entries } : null
94
- }
95
- return null
96
- })
97
-
98
- const [subagents] = createResource(
99
- () => showSubagents(),
100
- async () => {
101
- try {
102
- const proc = (props.api as any).client?.process
103
- if (typeof proc?.exec !== 'function') return null
104
- const result = await proc.exec({
105
- command: 'opencode',
106
- args: ['session', 'list', '--format', 'json'],
107
- timeoutMs: 5000,
108
- })
109
- const stdout = (result.stdout ?? result.output ?? '') as string
110
- const sessions = JSON.parse(stdout)
111
- if (!Array.isArray(sessions)) return null
112
- return sessions
113
- .filter((s: any) => s.id !== props.sessionID)
114
- .sort((a: any, b: any) => (b.updated ?? 0) - (a.updated ?? 0))
115
- .slice(0, 10)
116
- } catch { return null }
117
- },
118
- )
304
+ const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
119
305
 
120
306
  return (
121
307
  <box flexDirection="column" width="100%">
122
- {/* Header */}
308
+ {/* ── Header ── */}
123
309
  <text fg={theme().accent} attributes={{ bold: true }}>
124
- {`Pantheon v${props.version}`}
310
+ {`Pantheon${props.version ? ` v${props.version}` : ''}`}
125
311
  </text>
126
-
127
312
  <Show when={branch()}>
128
- {(b) => (<box marginTop={1}><text fg={theme().textMuted}>{b()}</text></box>)}
313
+ {(b) => <text fg={theme().textMuted}>{b()}</text>}
129
314
  </Show>
130
315
 
131
- {/* Subagents */}
132
- <box marginTop={0} onMouseDown={() => setShowSubagents((x) => !x)}>
316
+ <HR />
317
+
318
+ {/* ── Context Bar (always visible) ── */}
319
+ <ContextBar api={props.api} sessionID={props.sessionID} />
320
+
321
+ <HR />
322
+
323
+ {/* ── Sessions ── */}
324
+ <box onMouseDown={() => setShowSessions((x) => !x)}>
133
325
  <text fg={theme().text} attributes={{ bold: true }}>
134
- {`${showSubagents() ? '\u25bc' : '\u25b6'} Subagents`}
326
+ {`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
135
327
  </text>
136
- <text fg={theme().textMuted}>{` (${String(props.api.state.session.count())})`}</text>
328
+ <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
137
329
  </box>
138
-
139
- <Show when={showSubagents()}>
140
- <Show when={subagents()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>{`sessions: ${String(props.api.state.session.count())}`}</text></box>}>
141
- {(sessions) => (
142
- <Show when={sessions().length > 0} fallback={<box marginLeft={1}><text fg={theme().textMuted}>(idle)</text></box>}>
143
- <box marginLeft={1} flexDirection="column">
144
- <For each={sessions().slice(0, 5)}>
145
- {(s: any) => (
146
- <text fg={theme().textMuted}>{`${s.id.slice(0, 8)}... ${s.title?.slice(0, 30) ?? ''}`}</text>
147
- )}
148
- </For>
149
- </box>
150
- </Show>
151
- )}
330
+ <Show when={showSessions()}>
331
+ <Show
332
+ when={recentSessions().length > 0}
333
+ fallback={<box marginLeft={1}><text fg={theme().textMuted}>No recent sessions</text></box>}
334
+ >
335
+ <box marginLeft={1} flexDirection="column">
336
+ <For each={recentSessions()}>
337
+ {(ses: any) => <SessionRow api={props.api} session={ses} />}
338
+ </For>
339
+ </box>
152
340
  </Show>
153
341
  </Show>
154
342
 
155
- {/* Commands */}
156
- <box marginTop={0} onMouseDown={() => setShowCommands((x) => !x)}>
343
+ {/* ── Commands ── */}
344
+ <box onMouseDown={() => setShowCommands((x) => !x)}>
157
345
  <text fg={theme().text} attributes={{ bold: true }}>
158
346
  {`${showCommands() ? '\u25bc' : '\u25b6'} Commands`}
159
347
  </text>
160
348
  <text fg={theme().textMuted}>{` (${String(COMMANDS.length)})`}</text>
161
349
  </box>
162
-
163
350
  <Show when={showCommands()}>
164
- <For each={COMMANDS}>
165
- {(cmd) => (
166
- <box marginLeft={1} onMouseDown={(e) => {
167
- e.stopPropagation()
168
- try {
169
- const cmdApi = (props.api as any).command
170
- const cmdName = cmd.name.replace('/', '')
171
- if (cmdApi?.trigger?.(cmdName)) return
172
- } catch {}
173
- props.api.ui?.toast?.({ title: 'Command', message: `Type ${cmd.name} in chat` })
174
- }}>
175
- <text fg={cmd.name === '/pantheon' ? theme().accent : theme().textMuted}>{cmd.name}</text>
176
- <text fg={theme().textMuted}>{` — ${cmd.desc}`}</text>
177
- </box>
178
- )}
179
- </For>
351
+ <box marginLeft={1} flexDirection="column">
352
+ <For each={COMMANDS}>
353
+ {(cmd) => (
354
+ <box onMouseDown={(e) => {
355
+ e.stopPropagation()
356
+ try {
357
+ const cmdApi = (props.api as any).command
358
+ const cmdName = cmd.name.replace('/', '')
359
+ if (cmdApi?.trigger?.(cmdName)) return
360
+ } catch { /* fall through to toast */ }
361
+ props.api.ui?.toast?.({ title: 'Command', message: `Type ${cmd.name} in chat` })
362
+ }}>
363
+ <text fg={cmd.name === '/pantheon' ? theme().accent : theme().textMuted}>
364
+ {cmd.name}
365
+ </text>
366
+ <text fg={theme().textMuted}>{` \u2014 ${cmd.desc}`}</text>
367
+ </box>
368
+ )}
369
+ </For>
370
+ </box>
180
371
  </Show>
181
372
 
182
- {/* Agents */}
183
- <box marginTop={0} onMouseDown={() => setShowAgents((x) => !x)}>
373
+ {/* ── Agents ── */}
374
+ <box onMouseDown={() => setShowAgents((x) => !x)}>
184
375
  <text fg={theme().text} attributes={{ bold: true }}>
185
376
  {`${showAgents() ? '\u25bc' : '\u25b6'} Agents`}
186
377
  </text>
187
378
  <text fg={theme().textMuted}>{` (${String(AGENTS.length)})`}</text>
188
379
  </box>
189
-
190
380
  <Show when={showAgents()}>
191
- <For each={AGENTS}>
192
- {(agent) => (
193
- <box marginLeft={1}>
194
- <text fg={agent.tier === 'premium' ? theme().accent : theme().textMuted}>
195
- {`${agent.tier === 'premium' ? '\u2726 ' : '\u00b7 '}${agent.name}`}
196
- </text>
197
- <text fg={theme().textMuted}>{` — ${agent.role}`}</text>
198
- </box>
199
- )}
200
- </For>
381
+ <box marginLeft={1} flexDirection="column">
382
+ <For each={AGENTS}>
383
+ {(agent) => (
384
+ <box>
385
+ <text fg={agent.tier === 'premium' ? theme().accent : theme().textMuted}>
386
+ {`${agent.tier === 'premium' ? '\u2726 ' : '\u00b7 '}${agent.name}`}
387
+ </text>
388
+ <text fg={theme().textMuted}>{` \u2014 ${agent.role}`}</text>
389
+ </box>
390
+ )}
391
+ </For>
392
+ </box>
201
393
  </Show>
202
394
 
203
- {/* Config */}
204
- <box marginTop={0} onMouseDown={() => setShowConfig((x) => !x)}>
395
+ {/* ── Config ── */}
396
+ <box onMouseDown={() => setShowConfig((x) => !x)}>
205
397
  <text fg={theme().text} attributes={{ bold: true }}>
206
398
  {`${showConfig() ? '\u25bc' : '\u25b6'} Config`}
207
399
  </text>
208
400
  </box>
209
-
210
401
  <Show when={showConfig()}>
211
- <Show when={configInfo()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>(no config data)</text></box>}>
402
+ <Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
212
403
  {(cfg) => (
213
404
  <box marginLeft={1} flexDirection="column">
214
- <text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)}`}</text>
405
+ <text fg={theme().textMuted}>{`MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
215
406
  <text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
216
407
  </box>
217
408
  )}
218
409
  </Show>
219
410
  </Show>
220
411
 
221
- {/* Memory */}
222
- <box marginTop={0} onMouseDown={() => setShowMemory((x) => !x)}>
223
- <text fg={theme().text} attributes={{ bold: true }}>
224
- {`${showMemory() ? '\u25bc' : '\u25b6'} Memory`}
412
+ {/* ── Memory (always visible summary) ── */}
413
+ <box marginTop={1}>
414
+ <text fg={theme().textMuted}>
415
+ {memoryCount() !== null
416
+ ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
+ : 'Memory: N/A'}
225
418
  </text>
226
419
  </box>
227
-
228
- <Show when={showMemory()}>
229
- <Show when={memoryInfo()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>(no data)</text></box>}>
230
- {(mem) => (<box marginLeft={1}><text fg={theme().textMuted}>{`Entries: ${String(mem().entries)}`}</text></box>)}
231
- </Show>
232
- </Show>
233
420
  </box>
234
421
  )
235
422
  }
236
423
 
424
+ /* ─── Plugin Registration ────────────────────────────────── */
425
+
237
426
  const tui: TuiPlugin = async (api, _options, _meta) => {
238
- const version = await readPantheonVersion(api)
427
+ const version = await detectVersion(api)
428
+
239
429
  api.slots.register({
240
430
  order: 900,
241
431
  slots: {