pantheon-opencode 1.1.1-beta.10 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.1.1-beta.10",
3
+ "version": "1.1.1-beta.11",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cA2WM,QAAQ;EAAoB"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cAsbM,QAAQ;EAAoB"}
@@ -1,5 +1,5 @@
1
1
  import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, setProp } from "@opentui/solid";
2
- import { For, Show, createMemo, createResource, createSignal } from "solid-js";
2
+ import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
3
3
  //#region src/index.tsx
4
4
  /** @jsxImportSource @opentui/solid */
5
5
  const BAR_WIDTH = 20;
@@ -172,6 +172,18 @@ async function detectVersion(api) {
172
172
  if (tag && tag !== "5.0.0") return tag;
173
173
  }
174
174
  } catch {}
175
+ try {
176
+ const proc = api.client?.process;
177
+ if (typeof proc?.exec === "function") {
178
+ const r = await proc.exec({
179
+ command: "opencode",
180
+ args: ["--version"],
181
+ timeoutMs: 3e3
182
+ });
183
+ const ver = (r.stdout ?? r.output ?? "").trim().replace(/^v/, "");
184
+ if (ver) return ver;
185
+ }
186
+ } catch {}
175
187
  return null;
176
188
  }
177
189
  function ContextBar(props) {
@@ -244,29 +256,28 @@ function ContextBar(props) {
244
256
  }
245
257
  });
246
258
  }
247
- async function fetchRecentSessions(api, sessionID) {
248
- try {
249
- const proc = api.client?.process;
250
- if (typeof proc?.exec !== "function") return null;
251
- const result = await proc.exec({
252
- command: "opencode",
253
- args: [
254
- "session",
255
- "list",
256
- "--format",
257
- "json",
258
- "--max-count",
259
- "20"
260
- ],
261
- timeoutMs: 5e3
262
- });
263
- const stdout = result.stdout ?? result.output ?? "";
264
- const sessions = JSON.parse(stdout);
265
- if (!Array.isArray(sessions)) return null;
266
- return sessions.filter((s) => s.id !== sessionID).sort((a, b) => (b.updated ?? 0) - (a.updated ?? 0)).slice(0, 8);
267
- } catch {
259
+ function SessionRow(props) {
260
+ const theme = () => props.api.theme.current;
261
+ const status = createMemo(() => {
262
+ try {
263
+ const s = props.api.state.session?.status?.(props.session.id);
264
+ if (s?.type) return s.type;
265
+ } catch {}
268
266
  return null;
269
- }
267
+ });
268
+ const statusIcon = createMemo(() => {
269
+ switch (status()) {
270
+ case "busy": return "● ";
271
+ case "retry": return "⚠ ";
272
+ default: return "";
273
+ }
274
+ });
275
+ return (() => {
276
+ var _el$8 = createElement("text");
277
+ insert(_el$8, () => `${statusIcon()}${props.session?.id?.slice(0, 8) ?? "????"}... ${props.session?.title?.slice(0, 26) ?? "(untitled)"}`);
278
+ effect((_$p) => setProp(_el$8, "fg", theme().textMuted, _$p));
279
+ return _el$8;
280
+ })();
270
281
  }
271
282
  function View(props) {
272
283
  const [showCommands, setShowCommands] = createSignal(false);
@@ -275,16 +286,47 @@ function View(props) {
275
286
  const [showSessions, setShowSessions] = createSignal(false);
276
287
  const theme = () => props.api.theme.current;
277
288
  const branch = createMemo(() => props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null);
278
- const totalSessions = createMemo(() => {
289
+ const [sessionList, { refetch: refetchSessions }] = createResource(async () => {
279
290
  try {
280
- return props.api.state.session.count();
291
+ return await props.api.client?.session?.list?.({ limit: 100 }) ?? { data: [] };
281
292
  } catch {
282
- return 0;
293
+ return { data: [] };
283
294
  }
284
295
  });
285
- const memoryCount = createMemo(() => {
286
- const mem = props.api.state.memory;
287
- return safeNum(mem?.entries) || safeNum(mem?.count);
296
+ const totalSessions = createMemo(() => {
297
+ const result = sessionList();
298
+ if (!result) return 0;
299
+ const data = result.data ?? result;
300
+ if (!Array.isArray(data)) return 0;
301
+ return data.filter((s) => !s.parentID).length;
302
+ });
303
+ const recentSessions = createMemo(() => {
304
+ const result = sessionList();
305
+ if (!result) return [];
306
+ const data = result.data ?? result;
307
+ if (!Array.isArray(data)) return [];
308
+ return data.filter((s) => !s.parentID && s.id !== props.sessionID).sort((a, b) => {
309
+ const ta = a.time?.updated ?? a.updated ?? 0;
310
+ return (b.time?.updated ?? b.updated ?? 0) - ta;
311
+ }).slice(0, 8);
312
+ });
313
+ const [memoryCount, setMemoryCount] = createSignal(null);
314
+ onMount(() => {
315
+ const cleanup = [];
316
+ try {
317
+ cleanup.push(props.api.event.on("session.status", refetchSessions));
318
+ cleanup.push(props.api.event.on("session.created", refetchSessions));
319
+ cleanup.push(props.api.event.on("session.updated", refetchSessions));
320
+ cleanup.push(props.api.event.on("session.deleted", refetchSessions));
321
+ } catch {}
322
+ try {
323
+ const ev = props.api.event;
324
+ if (typeof ev?.on === "function") {
325
+ cleanup.push(ev.on("memory.created", () => setMemoryCount((c) => c !== null ? c + 1 : 1)));
326
+ cleanup.push(ev.on("memory.deleted", () => setMemoryCount((c) => c !== null ? Math.max(0, c - 1) : 0)));
327
+ }
328
+ } catch {}
329
+ onCleanup(() => cleanup.forEach((fn) => fn()));
288
330
  });
289
331
  const configSummary = createMemo(() => {
290
332
  const cfg = props.api.state.config;
@@ -295,104 +337,105 @@ function View(props) {
295
337
  autoCompaction: cfg.compaction?.auto === true
296
338
  };
297
339
  });
298
- const [sessions] = createResource(() => showSessions(), () => fetchRecentSessions(props.api, props.sessionID));
299
340
  const HR = () => (() => {
300
- var _el$8 = createElement("text");
301
- insertNode(_el$8, createTextNode(`────────────────────────────`));
302
- effect((_$p) => setProp(_el$8, "fg", theme().textMuted, _$p));
303
- return _el$8;
341
+ var _el$9 = createElement("text");
342
+ insertNode(_el$9, createTextNode(`────────────────────────────`));
343
+ effect((_$p) => setProp(_el$9, "fg", theme().textMuted, _$p));
344
+ return _el$9;
304
345
  })();
305
346
  return (() => {
306
- var _el$0 = createElement("box"), _el$1 = createElement("text"), _el$10 = createElement("box"), _el$11 = createElement("text"), _el$12 = createElement("text"), _el$13 = createElement("box"), _el$14 = createElement("text"), _el$15 = createElement("text"), _el$17 = createElement("box"), _el$18 = createElement("text"), _el$19 = createElement("text"), _el$21 = createElement("box"), _el$22 = createElement("text"), _el$23 = createElement("box"), _el$24 = createElement("text");
307
- insertNode(_el$0, _el$1);
308
- insertNode(_el$0, _el$10);
309
- insertNode(_el$0, _el$13);
310
- insertNode(_el$0, _el$17);
311
- insertNode(_el$0, _el$21);
312
- insertNode(_el$0, _el$23);
313
- setProp(_el$0, "flexDirection", "column");
314
- setProp(_el$0, "width", "100%");
315
- setProp(_el$1, "attributes", { bold: true });
316
- insert(_el$1, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
317
- insert(_el$0, createComponent(Show, {
347
+ var _el$1 = createElement("box"), _el$10 = createElement("text"), _el$11 = createElement("box"), _el$12 = createElement("text"), _el$13 = createElement("text"), _el$15 = createElement("box"), _el$16 = createElement("text"), _el$17 = createElement("text"), _el$19 = createElement("box"), _el$20 = createElement("text"), _el$21 = createElement("text"), _el$23 = createElement("box"), _el$24 = createElement("text"), _el$25 = createElement("box"), _el$26 = createElement("text");
348
+ insertNode(_el$1, _el$10);
349
+ insertNode(_el$1, _el$11);
350
+ insertNode(_el$1, _el$15);
351
+ insertNode(_el$1, _el$19);
352
+ insertNode(_el$1, _el$23);
353
+ insertNode(_el$1, _el$25);
354
+ setProp(_el$1, "flexDirection", "column");
355
+ setProp(_el$1, "width", "100%");
356
+ setProp(_el$10, "attributes", { bold: true });
357
+ insert(_el$10, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
358
+ insert(_el$1, createComponent(Show, {
318
359
  get when() {
319
360
  return branch();
320
361
  },
321
362
  children: (b) => (() => {
322
- var _el$25 = createElement("text");
323
- insert(_el$25, b);
324
- effect((_$p) => setProp(_el$25, "fg", theme().textMuted, _$p));
325
- return _el$25;
363
+ var _el$27 = createElement("text");
364
+ insert(_el$27, b);
365
+ effect((_$p) => setProp(_el$27, "fg", theme().textMuted, _$p));
366
+ return _el$27;
326
367
  })()
327
- }), _el$10);
328
- insert(_el$0, createComponent(HR, {}), _el$10);
329
- insert(_el$0, createComponent(ContextBar, {
368
+ }), _el$11);
369
+ insert(_el$1, createComponent(HR, {}), _el$11);
370
+ insert(_el$1, createComponent(ContextBar, {
330
371
  get api() {
331
372
  return props.api;
332
373
  },
333
374
  get sessionID() {
334
375
  return props.sessionID;
335
376
  }
336
- }), _el$10);
337
- insert(_el$0, createComponent(HR, {}), _el$10);
338
- insertNode(_el$10, _el$11);
339
- insertNode(_el$10, _el$12);
340
- setProp(_el$10, "onMouseDown", () => setShowSessions((x) => !x));
341
- setProp(_el$11, "attributes", { bold: true });
342
- insert(_el$11, () => `${showSessions() ? "▼" : "▶"} Sessions`);
343
- insert(_el$12, () => ` (${String(totalSessions())})`);
344
- insert(_el$0, createComponent(Show, {
377
+ }), _el$11);
378
+ insert(_el$1, createComponent(HR, {}), _el$11);
379
+ insertNode(_el$11, _el$12);
380
+ insertNode(_el$11, _el$13);
381
+ setProp(_el$11, "onMouseDown", () => setShowSessions((x) => !x));
382
+ setProp(_el$12, "attributes", { bold: true });
383
+ insert(_el$12, () => `${showSessions() ? "▼" : "▶"} Sessions`);
384
+ insert(_el$13, () => ` (${String(totalSessions())})`);
385
+ insert(_el$1, createComponent(Show, {
345
386
  get when() {
346
- return memo(() => !!showSessions())() && sessions();
387
+ return showSessions();
347
388
  },
348
- children: (s) => createComponent(Show, {
349
- get when() {
350
- return s().length > 0;
351
- },
352
- get fallback() {
353
- return (() => {
354
- var _el$27 = createElement("box"), _el$28 = createElement("text");
355
- insertNode(_el$27, _el$28);
356
- setProp(_el$27, "marginLeft", 1);
357
- insertNode(_el$28, createTextNode(`No recent sessions`));
358
- effect((_$p) => setProp(_el$28, "fg", theme().textMuted, _$p));
359
- return _el$27;
360
- })();
361
- },
362
- get children() {
363
- var _el$26 = createElement("box");
364
- setProp(_el$26, "marginLeft", 1);
365
- setProp(_el$26, "flexDirection", "column");
366
- insert(_el$26, createComponent(For, {
367
- get each() {
368
- return s();
369
- },
370
- children: (ses) => (() => {
371
- var _el$30 = createElement("text");
372
- insert(_el$30, () => `${ses.id.slice(0, 8)}... ${ses.title?.slice(0, 28) ?? "(untitled)"}`);
373
- effect((_$p) => setProp(_el$30, "fg", theme().textMuted, _$p));
374
- return _el$30;
375
- })()
376
- }));
377
- return _el$26;
378
- }
379
- })
380
- }), _el$13);
381
- insertNode(_el$13, _el$14);
382
- insertNode(_el$13, _el$15);
383
- setProp(_el$13, "onMouseDown", () => setShowCommands((x) => !x));
384
- setProp(_el$14, "attributes", { bold: true });
385
- insert(_el$14, () => `${showCommands() ? "▼" : "▶"} Commands`);
386
- insert(_el$15, () => ` (${String(COMMANDS.length)})`);
387
- insert(_el$0, createComponent(Show, {
389
+ get children() {
390
+ return createComponent(Show, {
391
+ get when() {
392
+ return recentSessions().length > 0;
393
+ },
394
+ get fallback() {
395
+ return (() => {
396
+ var _el$28 = createElement("box"), _el$29 = createElement("text");
397
+ insertNode(_el$28, _el$29);
398
+ setProp(_el$28, "marginLeft", 1);
399
+ insertNode(_el$29, createTextNode(`No recent sessions`));
400
+ effect((_$p) => setProp(_el$29, "fg", theme().textMuted, _$p));
401
+ return _el$28;
402
+ })();
403
+ },
404
+ get children() {
405
+ var _el$14 = createElement("box");
406
+ setProp(_el$14, "marginLeft", 1);
407
+ setProp(_el$14, "flexDirection", "column");
408
+ insert(_el$14, createComponent(For, {
409
+ get each() {
410
+ return recentSessions();
411
+ },
412
+ children: (ses) => createComponent(SessionRow, {
413
+ get api() {
414
+ return props.api;
415
+ },
416
+ session: ses
417
+ })
418
+ }));
419
+ return _el$14;
420
+ }
421
+ });
422
+ }
423
+ }), _el$15);
424
+ insertNode(_el$15, _el$16);
425
+ insertNode(_el$15, _el$17);
426
+ setProp(_el$15, "onMouseDown", () => setShowCommands((x) => !x));
427
+ setProp(_el$16, "attributes", { bold: true });
428
+ insert(_el$16, () => `${showCommands() ? "▼" : "▶"} Commands`);
429
+ insert(_el$17, () => ` (${String(COMMANDS.length)})`);
430
+ insert(_el$1, createComponent(Show, {
388
431
  get when() {
389
432
  return showCommands();
390
433
  },
391
434
  get children() {
392
- var _el$16 = createElement("box");
393
- setProp(_el$16, "marginLeft", 1);
394
- setProp(_el$16, "flexDirection", "column");
395
- insert(_el$16, createComponent(For, {
435
+ var _el$18 = createElement("box");
436
+ setProp(_el$18, "marginLeft", 1);
437
+ setProp(_el$18, "flexDirection", "column");
438
+ insert(_el$18, createComponent(For, {
396
439
  each: COMMANDS,
397
440
  children: (cmd) => (() => {
398
441
  var _el$31 = createElement("box"), _el$32 = createElement("text"), _el$33 = createElement("text");
@@ -424,24 +467,24 @@ function View(props) {
424
467
  return _el$31;
425
468
  })()
426
469
  }));
427
- return _el$16;
470
+ return _el$18;
428
471
  }
429
- }), _el$17);
430
- insertNode(_el$17, _el$18);
431
- insertNode(_el$17, _el$19);
432
- setProp(_el$17, "onMouseDown", () => setShowAgents((x) => !x));
433
- setProp(_el$18, "attributes", { bold: true });
434
- insert(_el$18, () => `${showAgents() ? "▼" : "▶"} Agents`);
435
- insert(_el$19, () => ` (${String(AGENTS.length)})`);
436
- insert(_el$0, createComponent(Show, {
472
+ }), _el$19);
473
+ insertNode(_el$19, _el$20);
474
+ insertNode(_el$19, _el$21);
475
+ setProp(_el$19, "onMouseDown", () => setShowAgents((x) => !x));
476
+ setProp(_el$20, "attributes", { bold: true });
477
+ insert(_el$20, () => `${showAgents() ? "▼" : "▶"} Agents`);
478
+ insert(_el$21, () => ` (${String(AGENTS.length)})`);
479
+ insert(_el$1, createComponent(Show, {
437
480
  get when() {
438
481
  return showAgents();
439
482
  },
440
483
  get children() {
441
- var _el$20 = createElement("box");
442
- setProp(_el$20, "marginLeft", 1);
443
- setProp(_el$20, "flexDirection", "column");
444
- insert(_el$20, createComponent(For, {
484
+ var _el$22 = createElement("box");
485
+ setProp(_el$22, "marginLeft", 1);
486
+ setProp(_el$22, "flexDirection", "column");
487
+ insert(_el$22, createComponent(For, {
445
488
  each: AGENTS,
446
489
  children: (agent) => (() => {
447
490
  var _el$34 = createElement("box"), _el$35 = createElement("text"), _el$36 = createElement("text");
@@ -461,14 +504,14 @@ function View(props) {
461
504
  return _el$34;
462
505
  })()
463
506
  }));
464
- return _el$20;
507
+ return _el$22;
465
508
  }
466
- }), _el$21);
467
- insertNode(_el$21, _el$22);
468
- setProp(_el$21, "onMouseDown", () => setShowConfig((x) => !x));
469
- setProp(_el$22, "attributes", { bold: true });
470
- insert(_el$22, () => `${showConfig() ? "▼" : "▶"} Config`);
471
- insert(_el$0, createComponent(Show, {
509
+ }), _el$23);
510
+ insertNode(_el$23, _el$24);
511
+ setProp(_el$23, "onMouseDown", () => setShowConfig((x) => !x));
512
+ setProp(_el$24, "attributes", { bold: true });
513
+ insert(_el$24, () => `${showConfig() ? "▼" : "▶"} Config`);
514
+ insert(_el$1, createComponent(Show, {
472
515
  get when() {
473
516
  return showConfig();
474
517
  },
@@ -508,21 +551,24 @@ function View(props) {
508
551
  })()
509
552
  });
510
553
  }
511
- }), _el$23);
512
- insertNode(_el$23, _el$24);
513
- setProp(_el$23, "marginTop", 1);
514
- insert(_el$24, () => `Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : "0 entries"}`);
554
+ }), _el$25);
555
+ insertNode(_el$25, _el$26);
556
+ setProp(_el$25, "marginTop", 1);
557
+ insert(_el$26, (() => {
558
+ var _c$ = memo(() => memoryCount() !== null);
559
+ return () => _c$() ? `Memory: ${fmtInt(memoryCount())} entries` : "Memory: N/A";
560
+ })());
515
561
  effect((_p$) => {
516
562
  var _v$3 = theme().accent, _v$4 = theme().text, _v$5 = theme().textMuted, _v$6 = theme().text, _v$7 = theme().textMuted, _v$8 = theme().text, _v$9 = theme().textMuted, _v$0 = theme().text, _v$1 = theme().textMuted;
517
- _v$3 !== _p$.e && (_p$.e = setProp(_el$1, "fg", _v$3, _p$.e));
518
- _v$4 !== _p$.t && (_p$.t = setProp(_el$11, "fg", _v$4, _p$.t));
519
- _v$5 !== _p$.a && (_p$.a = setProp(_el$12, "fg", _v$5, _p$.a));
520
- _v$6 !== _p$.o && (_p$.o = setProp(_el$14, "fg", _v$6, _p$.o));
521
- _v$7 !== _p$.i && (_p$.i = setProp(_el$15, "fg", _v$7, _p$.i));
522
- _v$8 !== _p$.n && (_p$.n = setProp(_el$18, "fg", _v$8, _p$.n));
523
- _v$9 !== _p$.s && (_p$.s = setProp(_el$19, "fg", _v$9, _p$.s));
524
- _v$0 !== _p$.h && (_p$.h = setProp(_el$22, "fg", _v$0, _p$.h));
525
- _v$1 !== _p$.r && (_p$.r = setProp(_el$24, "fg", _v$1, _p$.r));
563
+ _v$3 !== _p$.e && (_p$.e = setProp(_el$10, "fg", _v$3, _p$.e));
564
+ _v$4 !== _p$.t && (_p$.t = setProp(_el$12, "fg", _v$4, _p$.t));
565
+ _v$5 !== _p$.a && (_p$.a = setProp(_el$13, "fg", _v$5, _p$.a));
566
+ _v$6 !== _p$.o && (_p$.o = setProp(_el$16, "fg", _v$6, _p$.o));
567
+ _v$7 !== _p$.i && (_p$.i = setProp(_el$17, "fg", _v$7, _p$.i));
568
+ _v$8 !== _p$.n && (_p$.n = setProp(_el$20, "fg", _v$8, _p$.n));
569
+ _v$9 !== _p$.s && (_p$.s = setProp(_el$21, "fg", _v$9, _p$.s));
570
+ _v$0 !== _p$.h && (_p$.h = setProp(_el$24, "fg", _v$0, _p$.h));
571
+ _v$1 !== _p$.r && (_p$.r = setProp(_el$26, "fg", _v$1, _p$.r));
526
572
  return _p$;
527
573
  }, {
528
574
  e: void 0,
@@ -535,7 +581,7 @@ function View(props) {
535
581
  h: void 0,
536
582
  r: void 0
537
583
  });
538
- return _el$0;
584
+ return _el$1;
539
585
  })();
540
586
  }
541
587
  const tui = async (api, _options, _meta) => {
@@ -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, createResource, 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
 
@@ -77,7 +77,13 @@ function buildBar(pct: number): string {
77
77
 
78
78
  /* ─── Version Detection ────────────────────────────────────
79
79
  * 100% dinâmico — zero fallback hardcoded.
80
- * Se falhar, retorna null e a View omite a versão. */
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 */
81
87
 
82
88
  async function detectVersion(api: TuiPluginApi): Promise<string | null> {
83
89
  // Try 1: package.json do projeto (via worktree path)
@@ -100,6 +106,17 @@ async function detectVersion(api: TuiPluginApi): Promise<string | null> {
100
106
  }
101
107
  } catch { /* fall through */ }
102
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
+
103
120
  return null // sem fallback — versão não aparece
104
121
  }
105
122
 
@@ -165,25 +182,32 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
165
182
  )
166
183
  }
167
184
 
168
- /* ─── Sessions List (loaded on expand) ───────────────────── */
185
+ /* ─── Session Row (single session with live status indicator) ─ */
169
186
 
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
+ 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
+ )
187
211
  }
188
212
 
189
213
  /* ─── Main Sidebar View ──────────────────────────────────── */
@@ -200,15 +224,73 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
200
224
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
201
225
  )
202
226
 
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)
203
238
  const totalSessions = createMemo(() => {
204
- try { return props.api.state.session.count() } catch { return 0 }
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
205
244
  })
206
245
 
207
- const memoryCount = createMemo(() => {
208
- const mem = (props.api.state as any).memory
209
- return safeNum(mem?.entries) || safeNum(mem?.count)
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)
210
260
  })
211
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 ──
212
294
  const configSummary = createMemo(() => {
213
295
  const cfg = (props.api.state as any).config
214
296
  if (!cfg) return null
@@ -219,12 +301,6 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
219
301
  }
220
302
  })
221
303
 
222
- // Load session list only when expanded
223
- const [sessions] = createResource(
224
- () => showSessions(),
225
- () => fetchRecentSessions(props.api, props.sessionID),
226
- )
227
-
228
304
  const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
229
305
 
230
306
  return (
@@ -251,20 +327,17 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
251
327
  </text>
252
328
  <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
253
329
  </box>
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
- )}
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>
268
341
  </Show>
269
342
 
270
343
  {/* ── Commands ── */}
@@ -339,7 +412,9 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
339
412
  {/* ── Memory (always visible summary) ── */}
340
413
  <box marginTop={1}>
341
414
  <text fg={theme().textMuted}>
342
- {`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
415
+ {memoryCount() !== null
416
+ ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
+ : 'Memory: N/A'}
343
418
  </text>
344
419
  </box>
345
420
  </box>
@@ -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, createResource, 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
 
@@ -77,7 +77,13 @@ function buildBar(pct: number): string {
77
77
 
78
78
  /* ─── Version Detection ────────────────────────────────────
79
79
  * 100% dinâmico — zero fallback hardcoded.
80
- * Se falhar, retorna null e a View omite a versão. */
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 */
81
87
 
82
88
  async function detectVersion(api: TuiPluginApi): Promise<string | null> {
83
89
  // Try 1: package.json do projeto (via worktree path)
@@ -100,6 +106,17 @@ async function detectVersion(api: TuiPluginApi): Promise<string | null> {
100
106
  }
101
107
  } catch { /* fall through */ }
102
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
+
103
120
  return null // sem fallback — versão não aparece
104
121
  }
105
122
 
@@ -165,25 +182,32 @@ function ContextBar(props: { api: TuiPluginApi; sessionID: string }) {
165
182
  )
166
183
  }
167
184
 
168
- /* ─── Sessions List (loaded on expand) ───────────────────── */
185
+ /* ─── Session Row (single session with live status indicator) ─ */
169
186
 
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
+ 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
+ )
187
211
  }
188
212
 
189
213
  /* ─── Main Sidebar View ──────────────────────────────────── */
@@ -200,15 +224,73 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
200
224
  props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null,
201
225
  )
202
226
 
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)
203
238
  const totalSessions = createMemo(() => {
204
- try { return props.api.state.session.count() } catch { return 0 }
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
205
244
  })
206
245
 
207
- const memoryCount = createMemo(() => {
208
- const mem = (props.api.state as any).memory
209
- return safeNum(mem?.entries) || safeNum(mem?.count)
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)
210
260
  })
211
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 ──
212
294
  const configSummary = createMemo(() => {
213
295
  const cfg = (props.api.state as any).config
214
296
  if (!cfg) return null
@@ -219,12 +301,6 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
219
301
  }
220
302
  })
221
303
 
222
- // Load session list only when expanded
223
- const [sessions] = createResource(
224
- () => showSessions(),
225
- () => fetchRecentSessions(props.api, props.sessionID),
226
- )
227
-
228
304
  const HR = () => <text fg={theme().textMuted}>{'\u2500'.repeat(28)}</text>
229
305
 
230
306
  return (
@@ -251,20 +327,17 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
251
327
  </text>
252
328
  <text fg={theme().textMuted}>{` (${String(totalSessions())})`}</text>
253
329
  </box>
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
- )}
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>
268
341
  </Show>
269
342
 
270
343
  {/* ── Commands ── */}
@@ -339,7 +412,9 @@ function View(props: { api: TuiPluginApi; sessionID: string; version: string | n
339
412
  {/* ── Memory (always visible summary) ── */}
340
413
  <box marginTop={1}>
341
414
  <text fg={theme().textMuted}>
342
- {`Memory: ${memoryCount() > 0 ? `${fmtInt(memoryCount())} entries` : '0 entries'}`}
415
+ {memoryCount() !== null
416
+ ? `Memory: ${fmtInt(memoryCount()!)} entries`
417
+ : 'Memory: N/A'}
343
418
  </text>
344
419
  </box>
345
420
  </box>