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

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, 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,159 @@ 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
+ async function detectVersion(api: TuiPluginApi): Promise<string | null> {
83
+ // Try 1: package.json do projeto (via worktree path)
84
+ try {
85
+ const wt = ((api.state as any).path?.worktree ?? '') as string
86
+ const fp = wt ? `${wt}/package.json` : 'package.json'
87
+ const result = await api.client.file.read({ query: { path: fp } })
88
+ const match = String(result?.content ?? '').match(/"version":\s*"([^"]+)"/)
89
+ if (match?.[1]) return match[1]
90
+ } catch { /* fall through */ }
91
+
92
+ // Try 2: git describe --tags
93
+ try {
94
+ const proc = (api as any).client?.process
95
+ if (typeof proc?.exec === 'function') {
96
+ const r = await proc.exec({ command: 'git', args: ['describe', '--tags', '--always'], timeoutMs: 3000 })
97
+ const stdout = (r.stdout ?? r.output ?? '') as string
98
+ const tag = stdout.trim().replace(/^v/, '').replace(/-\d+-g[0-9a-f]+$/, '')
99
+ if (tag && tag !== '5.0.0') return tag
100
+ }
101
+ } catch { /* fall through */ }
102
+
103
+ return null // sem fallback — versão não aparece
104
+ }
105
+
106
+ /* ─── Context Bar ──────────────────────────────────────────
107
+ * Inspired by streetturtle/opencode-better-sidebar (context-progress)
108
+ * Shows: progress bar, token usage / limit, session cost */
109
+
110
+ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
111
+ const messages = createMemo((): any[] => {
112
+ try { return (props.api.state.session as any).messages(props.sessionID) ?? [] }
113
+ catch { return [] }
114
+ })
115
+
116
+ const usage = createMemo(() => {
117
+ const msgs = messages()
118
+ const lastAssistant = [...msgs].reverse().find((m: any) => {
119
+ const role = m?.role ?? m?.info?.role
120
+ return role === 'assistant' && safeNum(m?.tokens?.output) > 0
121
+ })
122
+ if (!lastAssistant) return null
123
+
124
+ const tokens = msgTokens(lastAssistant)
125
+ const pid = lastAssistant?.providerID ?? lastAssistant?.info?.providerID
126
+ const mid = lastAssistant?.modelID ?? lastAssistant?.info?.modelID
127
+ const provider = pid
128
+ ? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
129
+ : null
130
+ const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
131
+ const limit = ctxLimit > 0 ? ctxLimit : 200000
132
+ return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
133
+ })
134
+
135
+ const totalCost = createMemo(() => {
136
+ const state = ((props.api.state as any).session)?.get?.(props.sessionID)
137
+ const fromState = msgCost(state)
138
+ if (fromState > 0) return fromState
139
+ return messages().reduce((sum: number, m: any) => sum + msgCost(m), 0)
140
+ })
141
+
142
+ const theme = () => props.api.theme.current
143
+
144
+ return (
145
+ <Show when={usage()}>
146
+ {(u) => {
147
+ const pct = u().percent
148
+ const c = theme()
149
+ const barColor = pct >= 90 ? c.error : pct >= 70 ? c.warning : c.accent
150
+ const costStr = fmtCost(totalCost())
151
+ return (
152
+ <box flexDirection="column">
153
+ <box flexDirection="row" gap={1}>
154
+ <text fg={c.text} attributes={{ bold: true }}>Context</text>
155
+ <text fg={barColor}>{buildBar(pct)}</text>
156
+ <text fg={barColor}>{`${pct}%`}</text>
157
+ </box>
158
+ <text fg={c.textMuted}>
159
+ {`${fmtInt(u().tokens)} / ${fmtInt(u().limit)} tok${costStr ? ` ${costStr}` : ''}`}
160
+ </text>
161
+ </box>
162
+ )
163
+ }}
164
+ </Show>
165
+ )
166
+ }
167
+
168
+ /* ─── Sessions List (loaded on expand) ───────────────────── */
169
+
170
+ async function fetchRecentSessions(api: TuiPluginApi, sessionID: string): Promise<any[] | null> {
171
+ try {
172
+ const proc = (api as any).client?.process
173
+ if (typeof proc?.exec !== 'function') return null
174
+ const result = await proc.exec({
175
+ command: 'opencode',
176
+ args: ['session', 'list', '--format', 'json', '--max-count', '20'],
177
+ timeoutMs: 5000,
178
+ })
179
+ const stdout = (result.stdout ?? result.output ?? '') as string
180
+ const sessions = JSON.parse(stdout)
181
+ if (!Array.isArray(sessions)) return null
182
+ return sessions
183
+ .filter((s: any) => s.id !== sessionID)
184
+ .sort((a: any, b: any) => (b.updated ?? 0) - (a.updated ?? 0))
185
+ .slice(0, 8)
186
+ } catch { return null }
187
+ }
188
+
189
+ /* ─── Main Sidebar View ──────────────────────────────────── */
190
+
191
+ function View(props: { api: TuiPluginApi; sessionID: string; version: string | null }) {
67
192
  const [showCommands, setShowCommands] = createSignal(false)
68
193
  const [showAgents, setShowAgents] = createSignal(false)
69
194
  const [showConfig, setShowConfig] = createSignal(false)
70
- const [showMemory, setShowMemory] = createSignal(false)
71
- const [showSubagents, setShowSubagents] = createSignal(false)
195
+ const [showSessions, setShowSessions] = createSignal(false)
72
196
 
73
197
  const theme = () => props.api.theme.current
74
198
 
@@ -76,166 +200,157 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
76
200
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
77
201
  )
78
202
 
79
- const configInfo = createMemo(() => {
203
+ const totalSessions = createMemo(() => {
204
+ try { return props.api.state.session.count() } catch { return 0 }
205
+ })
206
+
207
+ const memoryCount = createMemo(() => {
208
+ const mem = (props.api.state as any).memory
209
+ return safeNum(mem?.entries) || safeNum(mem?.count)
210
+ })
211
+
212
+ const configSummary = createMemo(() => {
80
213
  const cfg = (props.api.state as any).config
81
214
  if (!cfg) return null
82
215
  return {
83
- plugins: Array.isArray(cfg.plugin) ? cfg.plugin : [],
84
216
  mcpCount: cfg.mcp ? Object.keys(cfg.mcp).length : 0,
217
+ pluginCount: Array.isArray(cfg.plugin) ? cfg.plugin.length : 0,
85
218
  autoCompaction: cfg.compaction?.auto === true,
86
219
  }
87
220
  })
88
221
 
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
- },
222
+ // Load session list only when expanded
223
+ const [sessions] = createResource(
224
+ () => showSessions(),
225
+ () => fetchRecentSessions(props.api, props.sessionID),
118
226
  )
119
227
 
228
+ const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
229
+
120
230
  return (
121
231
  <box flexDirection="column" width="100%">
122
- {/* Header */}
232
+ {/* ── Header ── */}
123
233
  <text fg={theme().accent} attributes={{ bold: true }}>
124
- {`Pantheon v${props.version}`}
234
+ {`Pantheon${props.version ? ` v${props.version}` : ''}`}
125
235
  </text>
126
-
127
236
  <Show when={branch()}>
128
- {(b) => (<box marginTop={1}><text fg={theme().textMuted}>{b()}</text></box>)}
237
+ {(b) => <text fg={theme().textMuted}>{b()}</text>}
129
238
  </Show>
130
239
 
131
- {/* Subagents */}
132
- <box marginTop={0} onMouseDown={() => setShowSubagents((x) => !x)}>
240
+ <HR />
241
+
242
+ {/* ── Context Bar (always visible) ── */}
243
+ <ContextBar api={props.api} sessionID={props.sessionID} />
244
+
245
+ <HR />
246
+
247
+ {/* ── Sessions ── */}
248
+ <box onMouseDown={() => setShowSessions((x) => !x)}>
133
249
  <text fg={theme().text} attributes={{ bold: true }}>
134
- {`${showSubagents() ? '\u25bc' : '\u25b6'} Subagents`}
250
+ {`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
135
251
  </text>
136
- <text fg={theme().textMuted}>{` (${String(props.api.state.session.count())})`}</text>
252
+ <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
137
253
  </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
- )}
152
- </Show>
254
+ <Show when={showSessions() && sessions()}>
255
+ {(s) => (
256
+ <Show when={s().length > 0} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No recent sessions</text></box>}>
257
+ <box marginLeft={1} flexDirection="column">
258
+ <For each={s()}>
259
+ {(ses: any) => (
260
+ <text fg={theme().textMuted}>
261
+ {`${ses.id.slice(0, 8)}... ${ses.title?.slice(0, 28) ?? '(untitled)'}`}
262
+ </text>
263
+ )}
264
+ </For>
265
+ </box>
266
+ </Show>
267
+ )}
153
268
  </Show>
154
269
 
155
- {/* Commands */}
156
- <box marginTop={0} onMouseDown={() => setShowCommands((x) => !x)}>
270
+ {/* ── Commands ── */}
271
+ <box onMouseDown={() => setShowCommands((x) => !x)}>
157
272
  <text fg={theme().text} attributes={{ bold: true }}>
158
273
  {`${showCommands() ? '\u25bc' : '\u25b6'} Commands`}
159
274
  </text>
160
275
  <text fg={theme().textMuted}>{` (${String(COMMANDS.length)})`}</text>
161
276
  </box>
162
-
163
277
  <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>
278
+ <box marginLeft={1} flexDirection="column">
279
+ <For each={COMMANDS}>
280
+ {(cmd) => (
281
+ <box onMouseDown={(e) => {
282
+ e.stopPropagation()
283
+ try {
284
+ const cmdApi = (props.api as any).command
285
+ const cmdName = cmd.name.replace('/', '')
286
+ if (cmdApi?.trigger?.(cmdName)) return
287
+ } catch { /* fall through to toast */ }
288
+ props.api.ui?.toast?.({ title: 'Command', message: `Type ${cmd.name} in chat` })
289
+ }}>
290
+ <text fg={cmd.name === '/pantheon' ? theme().accent : theme().textMuted}>
291
+ {cmd.name}
292
+ </text>
293
+ <text fg={theme().textMuted}>{` \u2014 ${cmd.desc}`}</text>
294
+ </box>
295
+ )}
296
+ </For>
297
+ </box>
180
298
  </Show>
181
299
 
182
- {/* Agents */}
183
- <box marginTop={0} onMouseDown={() => setShowAgents((x) => !x)}>
300
+ {/* ── Agents ── */}
301
+ <box onMouseDown={() => setShowAgents((x) => !x)}>
184
302
  <text fg={theme().text} attributes={{ bold: true }}>
185
303
  {`${showAgents() ? '\u25bc' : '\u25b6'} Agents`}
186
304
  </text>
187
305
  <text fg={theme().textMuted}>{` (${String(AGENTS.length)})`}</text>
188
306
  </box>
189
-
190
307
  <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>
308
+ <box marginLeft={1} flexDirection="column">
309
+ <For each={AGENTS}>
310
+ {(agent) => (
311
+ <box>
312
+ <text fg={agent.tier === 'premium' ? theme().accent : theme().textMuted}>
313
+ {`${agent.tier === 'premium' ? '\u2726 ' : '\u00b7 '}${agent.name}`}
314
+ </text>
315
+ <text fg={theme().textMuted}>{` \u2014 ${agent.role}`}</text>
316
+ </box>
317
+ )}
318
+ </For>
319
+ </box>
201
320
  </Show>
202
321
 
203
- {/* Config */}
204
- <box marginTop={0} onMouseDown={() => setShowConfig((x) => !x)}>
322
+ {/* ── Config ── */}
323
+ <box onMouseDown={() => setShowConfig((x) => !x)}>
205
324
  <text fg={theme().text} attributes={{ bold: true }}>
206
325
  {`${showConfig() ? '\u25bc' : '\u25b6'} Config`}
207
326
  </text>
208
327
  </box>
209
-
210
328
  <Show when={showConfig()}>
211
- <Show when={configInfo()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>(no config data)</text></box>}>
329
+ <Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
212
330
  {(cfg) => (
213
331
  <box marginLeft={1} flexDirection="column">
214
- <text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)}`}</text>
332
+ <text fg={theme().textMuted}>{`MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
215
333
  <text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
216
334
  </box>
217
335
  )}
218
336
  </Show>
219
337
  </Show>
220
338
 
221
- {/* Memory */}
222
- <box marginTop={0} onMouseDown={() => setShowMemory((x) => !x)}>
223
- <text fg={theme().text} attributes={{ bold: true }}>
224
- {`${showMemory() ? '\u25bc' : '\u25b6'} Memory`}
339
+ {/* ── Memory (always visible summary) ── */}
340
+ <box marginTop={1}>
341
+ <text fg={theme().textMuted}>
342
+ {`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
225
343
  </text>
226
344
  </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
345
  </box>
234
346
  )
235
347
  }
236
348
 
349
+ /* ─── Plugin Registration ────────────────────────────────── */
350
+
237
351
  const tui: TuiPlugin = async (api, _options, _meta) => {
238
- const version = await readPantheonVersion(api)
352
+ const version = await detectVersion(api)
353
+
239
354
  api.slots.register({
240
355
  order: 900,
241
356
  slots: {