pantheon-opencode 1.1.1-beta.6 → 1.1.1-beta.8
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/package.json +1 -1
- 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 +489 -137
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +182 -123
- package/src/plugins/tui/package-lock.json +2879 -0
- package/src/plugins/tui/package.json +9 -6
- package/src/plugins/tui/src/index.tsx +182 -123
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"src"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "
|
|
15
|
+
"build": "tsdown && cp src/index.tsx dist/tui.tsx",
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
@@ -22,11 +22,14 @@
|
|
|
22
22
|
"@opentui/solid": ">=0.4.5",
|
|
23
23
|
"solid-js": ">=1.9.0"
|
|
24
24
|
},
|
|
25
|
-
"devDependencies": {
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"rolldown-plugin-solid": "^0.2.1",
|
|
27
|
+
"tsdown": "^0.22.14"
|
|
28
|
+
},
|
|
26
29
|
"dependencies": {
|
|
27
|
-
"@opencode-ai/plugin": "
|
|
28
|
-
"@opentui/core": "
|
|
29
|
-
"@opentui/solid": "
|
|
30
|
-
"solid-js": "
|
|
30
|
+
"@opencode-ai/plugin": ">=1.14.0",
|
|
31
|
+
"@opentui/core": ">=0.4.5",
|
|
32
|
+
"@opentui/solid": ">=0.4.5",
|
|
33
|
+
"solid-js": ">=1.9.0"
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -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,
|
|
6
|
+
import { createMemo, createSignal, For, 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 */ }
|
|
8
|
+
/* ─── Constants ─────────────────────────────────────────── */
|
|
17
9
|
|
|
18
|
-
|
|
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 */ }
|
|
27
|
-
|
|
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,15 +40,110 @@ 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
|
|
|
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
|
+
/* ─── Context Bar ──────────────────────────────────────────
|
|
79
|
+
* Inspired by streetturtle/opencode-better-sidebar (context-progress)
|
|
80
|
+
* Shows: progress bar, token usage / limit, session cost
|
|
81
|
+
* Always visible at the top of the sidebar */
|
|
82
|
+
|
|
83
|
+
function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
|
|
84
|
+
const messages = createMemo((): any[] => {
|
|
85
|
+
try { return (props.api.state.session as any).messages(props.sessionID) ?? [] }
|
|
86
|
+
catch { return [] }
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const usage = createMemo(() => {
|
|
90
|
+
const msgs = messages()
|
|
91
|
+
const lastAssistant = [...msgs].reverse().find((m: any) => {
|
|
92
|
+
const role = m?.role ?? m?.info?.role
|
|
93
|
+
return role === 'assistant' && safeNum(m?.tokens?.output) > 0
|
|
94
|
+
})
|
|
95
|
+
if (!lastAssistant) return null
|
|
96
|
+
|
|
97
|
+
const tokens = msgTokens(lastAssistant)
|
|
98
|
+
const pid = lastAssistant?.providerID ?? lastAssistant?.info?.providerID
|
|
99
|
+
const mid = lastAssistant?.modelID ?? lastAssistant?.info?.modelID
|
|
100
|
+
const provider = pid
|
|
101
|
+
? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
|
|
102
|
+
: null
|
|
103
|
+
const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
|
|
104
|
+
const limit = ctxLimit > 0 ? ctxLimit : 128000
|
|
105
|
+
return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const totalCost = createMemo(() => {
|
|
109
|
+
const state = ((props.api.state as any).session)?.get?.(props.sessionID)
|
|
110
|
+
const fromState = msgCost(state)
|
|
111
|
+
if (fromState > 0) return fromState
|
|
112
|
+
return messages().reduce((sum: number, m: any) => sum + msgCost(m), 0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const theme = () => props.api.theme.current
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Show when={usage()}>
|
|
119
|
+
{(u) => {
|
|
120
|
+
const pct = u().percent
|
|
121
|
+
const c = theme()
|
|
122
|
+
const barColor = pct >= 90 ? c.error : pct >= 70 ? c.warning : c.accent
|
|
123
|
+
const costStr = fmtCost(totalCost())
|
|
124
|
+
return (
|
|
125
|
+
<box flexDirection="column">
|
|
126
|
+
<box flexDirection="row" gap={1}>
|
|
127
|
+
<text fg={c.text} attributes={{ bold: true }}>Context</text>
|
|
128
|
+
<text fg={barColor}>{buildBar(pct)}</text>
|
|
129
|
+
<text fg={barColor}>{`${pct}%`}</text>
|
|
130
|
+
</box>
|
|
131
|
+
<text fg={c.textMuted}>
|
|
132
|
+
{`${fmtInt(u().tokens)} / ${fmtInt(u().limit)} tok${costStr ? ` ${costStr}` : ''}`}
|
|
133
|
+
</text>
|
|
134
|
+
</box>
|
|
135
|
+
)
|
|
136
|
+
}}
|
|
137
|
+
</Show>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ─── Main Sidebar View ──────────────────────────────────── */
|
|
142
|
+
|
|
66
143
|
function View(props: { api: TuiPluginApi; sessionID: string; version: string }) {
|
|
67
144
|
const [showCommands, setShowCommands] = createSignal(false)
|
|
68
145
|
const [showAgents, setShowAgents] = createSignal(false)
|
|
69
146
|
const [showConfig, setShowConfig] = createSignal(false)
|
|
70
|
-
const [showMemory, setShowMemory] = createSignal(false)
|
|
71
147
|
const [showSubagents, setShowSubagents] = createSignal(false)
|
|
72
148
|
|
|
73
149
|
const theme = () => props.api.theme.current
|
|
@@ -76,166 +152,149 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
|
|
|
76
152
|
props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
|
|
77
153
|
)
|
|
78
154
|
|
|
79
|
-
const
|
|
155
|
+
const sessionCount = createMemo(() => {
|
|
156
|
+
try { return props.api.state.session.count() } catch { return 0 }
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const memoryCount = createMemo(() => {
|
|
160
|
+
const mem = (props.api.state as any).memory
|
|
161
|
+
return safeNum(mem?.entries) || safeNum(mem?.count)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const configSummary = createMemo(() => {
|
|
80
165
|
const cfg = (props.api.state as any).config
|
|
81
166
|
if (!cfg) return null
|
|
82
167
|
return {
|
|
83
|
-
plugins: Array.isArray(cfg.plugin) ? cfg.plugin : [],
|
|
84
168
|
mcpCount: cfg.mcp ? Object.keys(cfg.mcp).length : 0,
|
|
169
|
+
pluginCount: Array.isArray(cfg.plugin) ? cfg.plugin.length : 0,
|
|
85
170
|
autoCompaction: cfg.compaction?.auto === true,
|
|
86
171
|
}
|
|
87
172
|
})
|
|
88
173
|
|
|
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
|
-
)
|
|
174
|
+
const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
|
|
119
175
|
|
|
120
176
|
return (
|
|
121
177
|
<box flexDirection="column" width="100%">
|
|
122
|
-
{/* Header */}
|
|
178
|
+
{/* ── Header ── */}
|
|
123
179
|
<text fg={theme().accent} attributes={{ bold: true }}>
|
|
124
180
|
{`Pantheon v${props.version}`}
|
|
125
181
|
</text>
|
|
126
|
-
|
|
127
182
|
<Show when={branch()}>
|
|
128
|
-
{(b) =>
|
|
183
|
+
{(b) => <text fg={theme().textMuted}>{b()}</text>}
|
|
129
184
|
</Show>
|
|
130
185
|
|
|
131
|
-
|
|
132
|
-
|
|
186
|
+
<HR />
|
|
187
|
+
|
|
188
|
+
{/* ── Context Bar (always visible) ── */}
|
|
189
|
+
<ContextBar api={props.api} sessionID={props.sessionID} />
|
|
190
|
+
|
|
191
|
+
<HR />
|
|
192
|
+
|
|
193
|
+
{/* ── Subagents ── */}
|
|
194
|
+
<box onMouseDown={() => setShowSubagents((x) => !x)}>
|
|
133
195
|
<text fg={theme().text} attributes={{ bold: true }}>
|
|
134
196
|
{`${showSubagents() ? '\u25bc' : '\u25b6'} Subagents`}
|
|
135
197
|
</text>
|
|
136
|
-
<text fg={theme().textMuted}>{` (${String(
|
|
198
|
+
<text fg={theme().textMuted}>{` (${String(sessionCount())})`}</text>
|
|
137
199
|
</box>
|
|
138
|
-
|
|
139
200
|
<Show when={showSubagents()}>
|
|
140
|
-
<
|
|
141
|
-
{(
|
|
142
|
-
|
|
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>
|
|
201
|
+
<box marginLeft={1}>
|
|
202
|
+
<text fg={theme().textMuted}>{`Active sessions: ${String(sessionCount())}`}</text>
|
|
203
|
+
</box>
|
|
153
204
|
</Show>
|
|
154
205
|
|
|
155
|
-
{/* Commands */}
|
|
156
|
-
<box
|
|
206
|
+
{/* ── Commands ── */}
|
|
207
|
+
<box onMouseDown={() => setShowCommands((x) => !x)}>
|
|
157
208
|
<text fg={theme().text} attributes={{ bold: true }}>
|
|
158
209
|
{`${showCommands() ? '\u25bc' : '\u25b6'} Commands`}
|
|
159
210
|
</text>
|
|
160
211
|
<text fg={theme().textMuted}>{` (${String(COMMANDS.length)})`}</text>
|
|
161
212
|
</box>
|
|
162
|
-
|
|
163
213
|
<Show when={showCommands()}>
|
|
164
|
-
<
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
e
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
214
|
+
<box marginLeft={1} flexDirection="column">
|
|
215
|
+
<For each={COMMANDS}>
|
|
216
|
+
{(cmd) => (
|
|
217
|
+
<box onMouseDown={(e) => {
|
|
218
|
+
e.stopPropagation()
|
|
219
|
+
try {
|
|
220
|
+
const cmdApi = (props.api as any).command
|
|
221
|
+
const cmdName = cmd.name.replace('/', '')
|
|
222
|
+
if (cmdApi?.trigger?.(cmdName)) return
|
|
223
|
+
} catch { /* fall through to toast */ }
|
|
224
|
+
props.api.ui?.toast?.({ title: 'Command', message: `Type ${cmd.name} in chat` })
|
|
225
|
+
}}>
|
|
226
|
+
<text fg={cmd.name === '/pantheon' ? theme().accent : theme().textMuted}>
|
|
227
|
+
{cmd.name}
|
|
228
|
+
</text>
|
|
229
|
+
<text fg={theme().textMuted}>{` \u2014 ${cmd.desc}`}</text>
|
|
230
|
+
</box>
|
|
231
|
+
)}
|
|
232
|
+
</For>
|
|
233
|
+
</box>
|
|
180
234
|
</Show>
|
|
181
235
|
|
|
182
|
-
{/* Agents */}
|
|
183
|
-
<box
|
|
236
|
+
{/* ── Agents ── */}
|
|
237
|
+
<box onMouseDown={() => setShowAgents((x) => !x)}>
|
|
184
238
|
<text fg={theme().text} attributes={{ bold: true }}>
|
|
185
239
|
{`${showAgents() ? '\u25bc' : '\u25b6'} Agents`}
|
|
186
240
|
</text>
|
|
187
241
|
<text fg={theme().textMuted}>{` (${String(AGENTS.length)})`}</text>
|
|
188
242
|
</box>
|
|
189
|
-
|
|
190
243
|
<Show when={showAgents()}>
|
|
191
|
-
<
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
244
|
+
<box marginLeft={1} flexDirection="column">
|
|
245
|
+
<For each={AGENTS}>
|
|
246
|
+
{(agent) => (
|
|
247
|
+
<box>
|
|
248
|
+
<text fg={agent.tier === 'premium' ? theme().accent : theme().textMuted}>
|
|
249
|
+
{`${agent.tier === 'premium' ? '\u2726 ' : '\u00b7 '}${agent.name}`}
|
|
250
|
+
</text>
|
|
251
|
+
<text fg={theme().textMuted}>{` \u2014 ${agent.role}`}</text>
|
|
252
|
+
</box>
|
|
253
|
+
)}
|
|
254
|
+
</For>
|
|
255
|
+
</box>
|
|
201
256
|
</Show>
|
|
202
257
|
|
|
203
|
-
{/* Config */}
|
|
204
|
-
<box
|
|
258
|
+
{/* ── Config ── */}
|
|
259
|
+
<box onMouseDown={() => setShowConfig((x) => !x)}>
|
|
205
260
|
<text fg={theme().text} attributes={{ bold: true }}>
|
|
206
261
|
{`${showConfig() ? '\u25bc' : '\u25b6'} Config`}
|
|
207
262
|
</text>
|
|
208
263
|
</box>
|
|
209
|
-
|
|
210
264
|
<Show when={showConfig()}>
|
|
211
|
-
<Show when={
|
|
265
|
+
<Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
|
|
212
266
|
{(cfg) => (
|
|
213
267
|
<box marginLeft={1} flexDirection="column">
|
|
214
|
-
<text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)}`}</text>
|
|
268
|
+
<text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
|
|
215
269
|
<text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
|
|
216
270
|
</box>
|
|
217
271
|
)}
|
|
218
272
|
</Show>
|
|
219
273
|
</Show>
|
|
220
274
|
|
|
221
|
-
{/* Memory */}
|
|
222
|
-
<box marginTop={
|
|
223
|
-
<text fg={theme().
|
|
224
|
-
{
|
|
275
|
+
{/* ── Memory (always visible summary) ── */}
|
|
276
|
+
<box marginTop={1}>
|
|
277
|
+
<text fg={theme().textMuted}>
|
|
278
|
+
{`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
|
|
225
279
|
</text>
|
|
226
280
|
</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
281
|
</box>
|
|
234
282
|
)
|
|
235
283
|
}
|
|
236
284
|
|
|
285
|
+
/* ─── Plugin Registration ────────────────────────────────── */
|
|
286
|
+
|
|
237
287
|
const tui: TuiPlugin = async (api, _options, _meta) => {
|
|
238
|
-
|
|
288
|
+
let version = '5.0.0'
|
|
289
|
+
try {
|
|
290
|
+
const wt = ((api.state as any).path?.worktree ?? '') as string
|
|
291
|
+
const fp = wt ? `${wt}/package.json` : 'package.json'
|
|
292
|
+
const result = await api.client.file.read({ query: { path: fp } })
|
|
293
|
+
const content = String(result?.content ?? '')
|
|
294
|
+
const match = content.match(/"version":\s*"([^"]+)"/)
|
|
295
|
+
if (match?.[1]) version = match[1]
|
|
296
|
+
} catch { /* use default */ }
|
|
297
|
+
|
|
239
298
|
api.slots.register({
|
|
240
299
|
order: 900,
|
|
241
300
|
slots: {
|