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.
- package/README.md +7 -2
- package/bin/pantheon-init.mjs +19 -6
- package/package.json +1 -1
- package/scripts/install/cli-ui.mjs +160 -0
- package/scripts/install/component.mjs +24 -0
- package/scripts/install/interactive.mjs +395 -0
- package/scripts/install/migrate-config.mjs +308 -0
- package/scripts/install/migrate.mjs +210 -30
- package/scripts/install/opencode.mjs +96 -78
- package/scripts/install/plugin.mjs +139 -0
- package/scripts/install/state.mjs +194 -0
- package/src/plugins/tui/dist/tui.d.ts +8 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui.js +603 -137
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +317 -127
- package/src/plugins/tui/package-lock.json +2879 -0
- package/src/plugins/tui/package.json +11 -8
- package/src/plugins/tui/src/index.tsx +317 -127
|
@@ -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,
|
|
6
|
+
import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
313
|
+
{(b) => <text fg={theme().textMuted}>{b()}</text>}
|
|
129
314
|
</Show>
|
|
130
315
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
{`${
|
|
326
|
+
{`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
|
|
135
327
|
</text>
|
|
136
|
-
<text fg={theme().textMuted}>{` (${String(
|
|
328
|
+
<text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
|
|
137
329
|
</box>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
<
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
e
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
-
<
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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={
|
|
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
|
|
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={
|
|
223
|
-
<text fg={theme().
|
|
224
|
-
{
|
|
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
|
|
427
|
+
const version = await detectVersion(api)
|
|
428
|
+
|
|
239
429
|
api.slots.register({
|
|
240
430
|
order: 900,
|
|
241
431
|
slots: {
|