pantheon-opencode 1.1.1-beta.8 → 1.1.1

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.
@@ -3,7 +3,7 @@
3
3
  /** @jsxImportSource @opentui/solid */
4
4
 
5
5
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from '@opencode-ai/plugin/tui'
6
- import { createMemo, createSignal, For, Show } from 'solid-js'
6
+ import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
7
7
 
8
8
  /* ─── Constants ─────────────────────────────────────────── */
9
9
 
@@ -75,10 +75,54 @@ function buildBar(pct: number): string {
75
75
  return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
76
76
  }
77
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
+
78
123
  /* ─── Context Bar ──────────────────────────────────────────
79
124
  * 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 */
125
+ * Shows: progress bar, token usage / limit, session cost */
82
126
 
83
127
  function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
84
128
  const messages = createMemo((): any[] => {
@@ -101,7 +145,7 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
101
145
  ? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
102
146
  : null
103
147
  const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
104
- const limit = ctxLimit > 0 ? ctxLimit : 128000
148
+ const limit = ctxLimit > 0 ? ctxLimit : 200000
105
149
  return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
106
150
  })
107
151
 
@@ -138,13 +182,41 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
138
182
  )
139
183
  }
140
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
+
141
213
  /* ─── Main Sidebar View ──────────────────────────────────── */
142
214
 
143
- function View(props: { api: TuiPluginApi; sessionID: string; version: string }) {
215
+ function View(props: { api: TuiPluginApi; sessionID: string; version: string | null }) {
144
216
  const [showCommands, setShowCommands] = createSignal(false)
145
217
  const [showAgents, setShowAgents] = createSignal(false)
146
218
  const [showConfig, setShowConfig] = createSignal(false)
147
- const [showSubagents, setShowSubagents] = createSignal(false)
219
+ const [showSessions, setShowSessions] = createSignal(false)
148
220
 
149
221
  const theme = () => props.api.theme.current
150
222
 
@@ -152,15 +224,73 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
152
224
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
153
225
  )
154
226
 
155
- const sessionCount = createMemo(() => {
156
- try { return props.api.state.session.count() } catch { return 0 }
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)
157
260
  })
158
261
 
159
- const memoryCount = createMemo(() => {
160
- const mem = (props.api.state as any).memory
161
- return safeNum(mem?.entries) || safeNum(mem?.count)
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()))
162
291
  })
163
292
 
293
+ // ── Config summary ──
164
294
  const configSummary = createMemo(() => {
165
295
  const cfg = (props.api.state as any).config
166
296
  if (!cfg) return null
@@ -177,7 +307,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
177
307
  <box flexDirection="column" width="100%">
178
308
  {/* ── Header ── */}
179
309
  <text fg={theme().accent} attributes={{ bold: true }}>
180
- {`Pantheon v${props.version}`}
310
+ {`Pantheon${props.version ? ` v${props.version}` : ''}`}
181
311
  </text>
182
312
  <Show when={branch()}>
183
313
  {(b) => <text fg={theme().textMuted}>{b()}</text>}
@@ -190,17 +320,24 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
190
320
 
191
321
  <HR />
192
322
 
193
- {/* ── Subagents ── */}
194
- <box onMouseDown={() => setShowSubagents((x) => !x)}>
323
+ {/* ── Sessions ── */}
324
+ <box onMouseDown={() => setShowSessions((x) => !x)}>
195
325
  <text fg={theme().text} attributes={{ bold: true }}>
196
- {`${showSubagents() ? '\u25bc' : '\u25b6'} Subagents`}
326
+ {`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
197
327
  </text>
198
- <text fg={theme().textMuted}>{` (${String(sessionCount())})`}</text>
328
+ <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
199
329
  </box>
200
- <Show when={showSubagents()}>
201
- <box marginLeft={1}>
202
- <text fg={theme().textMuted}>{`Active sessions: ${String(sessionCount())}`}</text>
203
- </box>
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>
340
+ </Show>
204
341
  </Show>
205
342
 
206
343
  {/* ── Commands ── */}
@@ -265,7 +402,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
265
402
  <Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
266
403
  {(cfg) => (
267
404
  <box marginLeft={1} flexDirection="column">
268
- <text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
405
+ <text fg={theme().textMuted}>{`MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
269
406
  <text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
270
407
  </box>
271
408
  )}
@@ -275,7 +412,9 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
275
412
  {/* ── Memory (always visible summary) ── */}
276
413
  <box marginTop={1}>
277
414
  <text fg={theme().textMuted}>
278
- {`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
415
+ {memoryCount() !== null
416
+ ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
+ : 'Memory: N/A'}
279
418
  </text>
280
419
  </box>
281
420
  </box>
@@ -285,15 +424,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
285
424
  /* ─── Plugin Registration ────────────────────────────────── */
286
425
 
287
426
  const tui: TuiPlugin = async (api, _options, _meta) => {
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 */ }
427
+ const version = await detectVersion(api)
297
428
 
298
429
  api.slots.register({
299
430
  order: 900,
@@ -3,7 +3,7 @@
3
3
  /** @jsxImportSource @opentui/solid */
4
4
 
5
5
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from '@opencode-ai/plugin/tui'
6
- import { createMemo, createSignal, For, Show } from 'solid-js'
6
+ import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
7
7
 
8
8
  /* ─── Constants ─────────────────────────────────────────── */
9
9
 
@@ -75,10 +75,54 @@ function buildBar(pct: number): string {
75
75
  return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
76
76
  }
77
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
+
78
123
  /* ─── Context Bar ──────────────────────────────────────────
79
124
  * 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 */
125
+ * Shows: progress bar, token usage / limit, session cost */
82
126
 
83
127
  function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
84
128
  const messages = createMemo((): any[] => {
@@ -101,7 +145,7 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
101
145
  ? ((props.api.state as any).provider ?? []).find((p: any) => p.id === pid)
102
146
  : null
103
147
  const ctxLimit = safeNum(provider?.models?.[mid ?? '']?.limit?.context)
104
- const limit = ctxLimit > 0 ? ctxLimit : 128000
148
+ const limit = ctxLimit > 0 ? ctxLimit : 200000
105
149
  return { tokens, limit, percent: ctxLimit > 0 ? Math.round((tokens / ctxLimit) * 100) : 0 }
106
150
  })
107
151
 
@@ -138,13 +182,41 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
138
182
  )
139
183
  }
140
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
+
141
213
  /* ─── Main Sidebar View ──────────────────────────────────── */
142
214
 
143
- function View(props: { api: TuiPluginApi; sessionID: string; version: string }) {
215
+ function View(props: { api: TuiPluginApi; sessionID: string; version: string | null }) {
144
216
  const [showCommands, setShowCommands] = createSignal(false)
145
217
  const [showAgents, setShowAgents] = createSignal(false)
146
218
  const [showConfig, setShowConfig] = createSignal(false)
147
- const [showSubagents, setShowSubagents] = createSignal(false)
219
+ const [showSessions, setShowSessions] = createSignal(false)
148
220
 
149
221
  const theme = () => props.api.theme.current
150
222
 
@@ -152,15 +224,73 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
152
224
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
153
225
  )
154
226
 
155
- const sessionCount = createMemo(() => {
156
- try { return props.api.state.session.count() } catch { return 0 }
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)
157
260
  })
158
261
 
159
- const memoryCount = createMemo(() => {
160
- const mem = (props.api.state as any).memory
161
- return safeNum(mem?.entries) || safeNum(mem?.count)
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()))
162
291
  })
163
292
 
293
+ // ── Config summary ──
164
294
  const configSummary = createMemo(() => {
165
295
  const cfg = (props.api.state as any).config
166
296
  if (!cfg) return null
@@ -177,7 +307,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
177
307
  <box flexDirection="column" width="100%">
178
308
  {/* ── Header ── */}
179
309
  <text fg={theme().accent} attributes={{ bold: true }}>
180
- {`Pantheon v${props.version}`}
310
+ {`Pantheon${props.version ? ` v${props.version}` : ''}`}
181
311
  </text>
182
312
  <Show when={branch()}>
183
313
  {(b) => <text fg={theme().textMuted}>{b()}</text>}
@@ -190,17 +320,24 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
190
320
 
191
321
  <HR />
192
322
 
193
- {/* ── Subagents ── */}
194
- <box onMouseDown={() => setShowSubagents((x) => !x)}>
323
+ {/* ── Sessions ── */}
324
+ <box onMouseDown={() => setShowSessions((x) => !x)}>
195
325
  <text fg={theme().text} attributes={{ bold: true }}>
196
- {`${showSubagents() ? '\u25bc' : '\u25b6'} Subagents`}
326
+ {`${showSessions() ? '\u25bc' : '\u25b6'} Sessions`}
197
327
  </text>
198
- <text fg={theme().textMuted}>{` (${String(sessionCount())})`}</text>
328
+ <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
199
329
  </box>
200
- <Show when={showSubagents()}>
201
- <box marginLeft={1}>
202
- <text fg={theme().textMuted}>{`Active sessions: ${String(sessionCount())}`}</text>
203
- </box>
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>
340
+ </Show>
204
341
  </Show>
205
342
 
206
343
  {/* ── Commands ── */}
@@ -265,7 +402,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
265
402
  <Show when={configSummary()} fallback={<box marginLeft={1}><text fg={theme().textMuted}>No config data</text></box>}>
266
403
  {(cfg) => (
267
404
  <box marginLeft={1} flexDirection="column">
268
- <text fg={theme().textMuted}>{`MCP servers: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
405
+ <text fg={theme().textMuted}>{`MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`}</text>
269
406
  <text fg={theme().textMuted}>{`Auto-compaction: ${cfg().autoCompaction ? 'ON' : 'OFF'}`}</text>
270
407
  </box>
271
408
  )}
@@ -275,7 +412,9 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
275
412
  {/* ── Memory (always visible summary) ── */}
276
413
  <box marginTop={1}>
277
414
  <text fg={theme().textMuted}>
278
- {`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
415
+ {memoryCount() !== null
416
+ ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
+ : 'Memory: N/A'}
279
418
  </text>
280
419
  </box>
281
420
  </box>
@@ -285,15 +424,7 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string })
285
424
  /* ─── Plugin Registration ────────────────────────────────── */
286
425
 
287
426
  const tui: TuiPlugin = async (api, _options, _meta) => {
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 */ }
427
+ const version = await detectVersion(api)
297
428
 
298
429
  api.slots.register({
299
430
  order: 900,