opencode-subagent-magazine 1.1.2 → 1.2.0

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": "opencode-subagent-magazine",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "types": "./dist/index.d.ts"
12
12
  },
13
13
  "./tui": {
14
- "import": "./src/index.tsx",
14
+ "import": "./dist/tui.js",
15
15
  "config": {
16
16
  "enabled": true
17
17
  }
@@ -27,7 +27,8 @@
27
27
  "opencode-subagent-magazine": "install.mjs"
28
28
  },
29
29
  "scripts": {
30
- "build": "tsc",
30
+ "build": "tsc && node build.tui.mjs",
31
+ "build:tui": "node build.tui.mjs",
31
32
  "typecheck": "tsc --noEmit",
32
33
  "version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
33
34
  "prepublishOnly": "tsc"
@@ -54,6 +55,8 @@
54
55
  "devDependencies": {
55
56
  "@opencode-ai/plugin": "^1.14.50",
56
57
  "@opencode-ai/sdk": "^1.14.50",
58
+ "esbuild": "^0.25.0",
59
+ "esbuild-plugin-solid": "^0.5.0",
57
60
  "tsx": "^4.22.3",
58
61
  "typescript": "^5.8.0"
59
62
  }
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.1.2";
2
+ export const PLUGIN_VERSION="1.2.0";
package/src/index.tsx CHANGED
@@ -45,6 +45,8 @@ interface SubEntry {
45
45
  }
46
46
 
47
47
  type Lang = "zh" | "en"
48
+ type SortOrder = "desc" | "asc"
49
+ type ScrollMode = "wheel" | "click"
48
50
 
49
51
  /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
50
52
  const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
@@ -68,10 +70,15 @@ const I18N: Record<Lang, Record<string, string>> = {
68
70
  "cost.label": "费用",
69
71
  "scroll.more": "更多",
70
72
  "scroll.top": "回顶",
73
+ "scroll.bottom": "回底",
71
74
  "dismiss.label": "标记完成",
72
75
  "status.running": "运行中",
73
76
  "status.done": "已完成",
74
77
  "status.error": "错误",
78
+ "order.desc": "降序(最新在前)",
79
+ "order.asc": "升序(最早在前)",
80
+ "scroll.wheel": "滚轮翻页",
81
+ "scroll.click": "点击翻页",
75
82
  },
76
83
  en: {
77
84
  "panel.title": "SubAgent",
@@ -87,10 +94,15 @@ const I18N: Record<Lang, Record<string, string>> = {
87
94
  "cost.label": "cost",
88
95
  "scroll.more": "more",
89
96
  "scroll.top": "Top",
97
+ "scroll.bottom": "Bottom",
90
98
  "dismiss.label": "dismiss",
91
99
  "status.running": "running",
92
100
  "status.done": "done",
93
101
  "status.error": "error",
102
+ "order.desc": "Desc (newest first)",
103
+ "order.asc": "Asc (oldest first)",
104
+ "scroll.wheel": "Wheel Scroll",
105
+ "scroll.click": "Click Scroll",
94
106
  },
95
107
  }
96
108
 
@@ -247,8 +259,7 @@ function safeErrorMsg(err: unknown): string {
247
259
  // Sidebar component
248
260
  // ===================================================================
249
261
 
250
- // 模块级缓存:保留各 session 的最新 entryMap,不随组件内 session 切换而丢失。
251
- // 当用户在子 session 内部时,handleSessionEnd 可通过此缓存找到父 session 的 entries。
262
+ // 模块级缓存:各 session entry 状态独立存储,不随当前视图切换而清除。
252
263
  const globalEntryCache = new Map<string, Map<string, SubEntry>>()
253
264
 
254
265
  function SubAgentPanel(props: {
@@ -256,6 +267,8 @@ function SubAgentPanel(props: {
256
267
  api: TuiPluginApi
257
268
  lang: () => Lang
258
269
  maxEntries: () => number
270
+ sortOrder: () => SortOrder
271
+ scrollMode: () => ScrollMode
259
272
  sessionId: string
260
273
  }): JSX.Element {
261
274
  const t = (key: string) => I18N[props.lang()][key] ?? key
@@ -347,8 +360,8 @@ function SubAgentPanel(props: {
347
360
  setEntryMapRaw((prev) => {
348
361
  const next = typeof arg === "function" ? (arg as Function)(prev) : arg
349
362
 
350
- // 检测是否有 entry 从 running 变为 done/error 立即写 KV
351
- // 避免 session 切换时 KV 仍是旧状态,导致 scan 用 Date.now() 覆盖正确的 endedAt
363
+ // entry 状态落定(done/error)时立即持久化到 KV,跳过常规 debounce,
364
+ // 确保跨视图的状态一致性。
352
365
  let needsImmediateFlush = false
353
366
  for (const [id, entry] of next) {
354
367
  const prevEntry = prev.get(id)
@@ -369,7 +382,7 @@ function SubAgentPanel(props: {
369
382
  persistEntries(props.sessionId, next)
370
383
  }
371
384
 
372
- // 同步到全局缓存,确保跨 session 查找时数据可用
385
+ // 同步到模块级缓存,供其他视图读取当前 session 的最新状态
373
386
  globalEntryCache.set(props.sessionId, new Map(next))
374
387
 
375
388
  return next
@@ -386,6 +399,8 @@ function SubAgentPanel(props: {
386
399
  const [hoveredOpen, setHoveredOpen] = createSignal<string | undefined>(undefined)
387
400
  const [hoveredDismiss, setHoveredDismiss] = createSignal<string | undefined>(undefined)
388
401
  const [hoveredTop, setHoveredTop] = createSignal(false)
402
+ const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false)
403
+ const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false)
389
404
  const [scrollOffset, setScrollOffset] = createSignal(
390
405
  (() => { try { return loadSessionData()[props.sessionId]?.scroll ?? 0 } catch { return 0 } })()
391
406
  )
@@ -723,14 +738,13 @@ function SubAgentPanel(props: {
723
738
  return changed ? next : prev
724
739
  })
725
740
 
726
- // session 完成事件:用户正在查看子 session 内部时,其他子代理 done。
727
- // 上面的 setEntryMap 在子 session 的 entries 中找不到父 session 的 entry
728
- // 通过全局缓存(切换前保留的父 session entries)查找并更新,再同步回 KV。
741
+ // 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
742
+ // 并更新父 session 的 entry 状态,随后写回 KV
729
743
  try {
730
744
  const sessionObj = props.api.state.session.get(sid)
731
745
  const parentSid = sessionObj?.parentID
732
746
  if (parentSid && parentSid !== props.sessionId) {
733
- // 优先全局缓存——切换 session 时不会被替换,始终保留父 session 的最新 entries
747
+ // 优先从模块级缓存获取父 session entries,不受当前视图切换影响
734
748
  const parentCache = globalEntryCache.get(parentSid)
735
749
  const nowTs = Date.now()
736
750
  let found = false
@@ -739,7 +753,7 @@ function SubAgentPanel(props: {
739
753
  found = tryMatchAndUpdate(parentCache, sid, status, nowTs)
740
754
  }
741
755
 
742
- // 缓存未命中时回退到 KV(极端情况:session 切换前缓存未建立)
756
+ // 缓存未命中时回退到 KV 读取
743
757
  if (!found) {
744
758
  const data = loadSessionData()
745
759
  const rec = data[parentSid]
@@ -747,7 +761,7 @@ function SubAgentPanel(props: {
747
761
  const fallbackMap = new Map(rec.entries.map((e: SubEntry) => [e.id, e]))
748
762
  found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs)
749
763
  if (found) {
750
- // 回退命中了也要写回 KV,并回填缓存
764
+ // 回退命中后写入 KV 并回填缓存
751
765
  data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] }
752
766
  saveSessionData(data)
753
767
  globalEntryCache.set(parentSid, fallbackMap)
@@ -755,7 +769,7 @@ function SubAgentPanel(props: {
755
769
  }
756
770
  }
757
771
 
758
- // KV 同步:以全局缓存(最新的内存状态)为准写回 KV
772
+ // 将模块级缓存中的最新状态同步到 KV
759
773
  if (found && parentCache) {
760
774
  const data = loadSessionData()
761
775
  data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] }
@@ -878,7 +892,7 @@ function SubAgentPanel(props: {
878
892
  // scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
879
893
  // Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
880
894
  setEntryMapRaw((prev) => {
881
- // 优先全局缓存——handleSessionEnd 修复2 已同步更新,无 KV 读写延迟
895
+ // 优先从模块级缓存加载,KV 仅作缓存未命中时的回退
882
896
  const next = switched
883
897
  ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
884
898
  : new Map(prev)
@@ -1022,7 +1036,11 @@ function SubAgentPanel(props: {
1022
1036
  // ── derived signals ──
1023
1037
  // Stable list — only changes when entryMap changes
1024
1038
  const entryList = createMemo(() => {
1025
- return [...entryMap().values()].sort((a, b) => b.startedAt - a.startedAt)
1039
+ const entries = [...entryMap().values()]
1040
+ if (props.sortOrder() === "desc") {
1041
+ return entries.sort((a, b) => b.startedAt - a.startedAt)
1042
+ }
1043
+ return entries.sort((a, b) => a.startedAt - b.startedAt)
1026
1044
  })
1027
1045
 
1028
1046
  const max = props.maxEntries
@@ -1036,6 +1054,48 @@ function SubAgentPanel(props: {
1036
1054
  const hiddenAbove = createMemo(() => clampedOffset())
1037
1055
  const hiddenBelow = createMemo(() => Math.max(0, entryList().length - clampedOffset() - max()))
1038
1056
 
1057
+ // Drop hover state when ↑ more disappears (hiddenAbove hits zero)
1058
+ createEffect(() => {
1059
+ if (hiddenAbove() === 0) setHoveredMoreAbove(false)
1060
+ })
1061
+
1062
+ // Reset scroll on sort order change: jump to newest in view
1063
+ let sortInitialized = false
1064
+ createEffect(() => {
1065
+ props.sortOrder()
1066
+ if (!sortInitialized) { sortInitialized = true; return }
1067
+ const total = untrack(() => entryList().length)
1068
+ const m = untrack(() => max())
1069
+ const target = props.sortOrder() === "desc" ? 0 : Math.max(0, total - m)
1070
+ setScrollOffset(target)
1071
+ setTimeout(() => {
1072
+ try { persistScroll(props.sessionId, target) } catch {}
1073
+ }, 0)
1074
+ })
1075
+
1076
+ // When new entries arrive while viewing the newest end, keep the view at newest
1077
+ let prevEntryCount = 0
1078
+ createEffect(() => {
1079
+ const total = entryList().length
1080
+ if (prevEntryCount === 0) { prevEntryCount = total; return }
1081
+ if (total === prevEntryCount) return
1082
+
1083
+ const m = max()
1084
+ const wasAtNewest = props.sortOrder() === "desc"
1085
+ ? untrack(() => scrollOffset() === 0)
1086
+ : untrack(() => scrollOffset() >= prevEntryCount - m)
1087
+
1088
+ prevEntryCount = total
1089
+
1090
+ if (wasAtNewest) {
1091
+ const target = props.sortOrder() === "desc" ? 0 : Math.max(0, total - m)
1092
+ setScrollOffset(target)
1093
+ setTimeout(() => {
1094
+ try { persistScroll(props.sessionId, target) } catch {}
1095
+ }, 0)
1096
+ }
1097
+ })
1098
+
1039
1099
  const entries = createMemo(() => {
1040
1100
  const nowVal = now()
1041
1101
  return entryList().map((e) => ({
@@ -1195,6 +1255,7 @@ function SubAgentPanel(props: {
1195
1255
  >
1196
1256
  <box
1197
1257
  onMouseScroll={(e) => {
1258
+ if (props.scrollMode() === "click") return
1198
1259
  const total = entryList().length
1199
1260
  const m = max()
1200
1261
  if (total <= m) return
@@ -1207,8 +1268,28 @@ function SubAgentPanel(props: {
1207
1268
  }}
1208
1269
  >
1209
1270
  <Show when={hiddenAbove() > 0}>
1210
- <text style={{ fg: pal().muted }}>
1211
- {" "}&uarr; {hiddenAbove()} {t("scroll.more")}
1271
+ <text
1272
+ onMouseOver={() => setHoveredMoreAbove(true)}
1273
+ onMouseOut={() => setHoveredMoreAbove(false)}
1274
+ onMouseUp={() => {
1275
+ const total = entryList().length
1276
+ const m = max()
1277
+ if (total <= m) return
1278
+ const next = Math.max(0, scrollOffset() - m)
1279
+ if (next === 0) {
1280
+ setTimeout(() => {
1281
+ setScrollOffset(next)
1282
+ try { persistScroll(props.sessionId, next) } catch {}
1283
+ }, 0)
1284
+ } else {
1285
+ setScrollOffset(next)
1286
+ try { persistScroll(props.sessionId, next) } catch {}
1287
+ }
1288
+ }}
1289
+ >
1290
+ <span style={{ fg: hoveredMoreAbove() ? pal().warning : pal().muted }}>
1291
+ {" "}&uarr; {hiddenAbove()} {t("scroll.more")}
1292
+ </span>
1212
1293
  </text>
1213
1294
  </Show>
1214
1295
  <For each={visibleList()}>
@@ -1396,23 +1477,52 @@ function SubAgentPanel(props: {
1396
1477
  )
1397
1478
  }}
1398
1479
  </For>
1399
- <Show when={hiddenBelow() > 0 || scrollOffset() > 0}>
1480
+ <Show when={hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max())}>
1400
1481
  {(() => {
1401
1482
  const showMore = hiddenBelow() > 0
1402
- const showTop = scrollOffset() > 0
1483
+ const showTop = props.sortOrder() === "desc"
1484
+ ? scrollOffset() > 0
1485
+ : entryList().length > max() && clampedOffset() < entryList().length - max()
1403
1486
  const left = showMore ? ` \u2193 ${hiddenBelow()} ${t("scroll.more")}` : " "
1404
- const right = `\u2191 ${t("scroll.top")}`
1487
+ const right = props.sortOrder() === "desc"
1488
+ ? `\u2191 ${t("scroll.top")}`
1489
+ : `\u2193 ${t("scroll.bottom")}`
1405
1490
  const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0
1406
1491
  return (
1407
1492
  <box flexDirection="row">
1408
- <text style={{ fg: pal().muted }}>{left}</text>
1493
+ <text
1494
+ onMouseOver={() => showMore && setHoveredMoreBelow(true)}
1495
+ onMouseOut={() => setHoveredMoreBelow(false)}
1496
+ onMouseUp={() => {
1497
+ if (!showMore) return
1498
+ const total = entryList().length
1499
+ const m = max()
1500
+ if (total <= m) return
1501
+ setScrollOffset((prev) => Math.min(total - m, prev + m))
1502
+ try { persistScroll(props.sessionId, scrollOffset()) } catch {}
1503
+ setHoveredMoreBelow(false)
1504
+ }}
1505
+ >
1506
+ <span style={{ fg: showMore && hoveredMoreBelow() ? pal().warning : pal().muted }}>
1507
+ {left}
1508
+ </span>
1509
+ </text>
1409
1510
  {showTop ? (
1410
1511
  <>
1411
1512
  <text style={{ fg: pal().muted }}>{" ".repeat(pad)}</text>
1412
1513
  <text
1413
1514
  onMouseOver={() => setHoveredTop(true)}
1414
1515
  onMouseOut={() => setHoveredTop(false)}
1415
- onMouseUp={() => { setScrollOffset(0); setHoveredTop(false) }}
1516
+ onMouseUp={() => {
1517
+ const total = entryList().length
1518
+ const m = max()
1519
+ if (props.sortOrder() === "desc") {
1520
+ setScrollOffset(0)
1521
+ } else {
1522
+ setScrollOffset(Math.max(0, total - m))
1523
+ }
1524
+ setHoveredTop(false)
1525
+ }}
1416
1526
  >
1417
1527
  <span style={{ fg: hoveredTop() ? pal().warning : pal().muted }}>{right}</span>
1418
1528
  </text>
@@ -1438,6 +1548,10 @@ interface SharedSignals {
1438
1548
  setLang: (l: Lang) => void
1439
1549
  maxEntries: () => number
1440
1550
  setMaxEntries: (n: number) => void
1551
+ sortOrder: () => SortOrder
1552
+ setSortOrder: (o: SortOrder) => void
1553
+ scrollMode: () => ScrollMode
1554
+ setScrollMode: (m: ScrollMode) => void
1441
1555
  sessionId: string
1442
1556
  }
1443
1557
 
@@ -1453,6 +1567,8 @@ function createSidebarSlot(api: TuiPluginApi, sig: SharedSignals): TuiSlotPlugin
1453
1567
  api={api}
1454
1568
  lang={sig.lang}
1455
1569
  maxEntries={sig.maxEntries}
1570
+ sortOrder={sig.sortOrder}
1571
+ scrollMode={sig.scrollMode}
1456
1572
  sessionId={input.session_id}
1457
1573
  />
1458
1574
  )
@@ -1472,8 +1588,14 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1472
1588
  const [maxEntries, setMaxEntries] = createSignal(
1473
1589
  parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
1474
1590
  )
1591
+ const [sortOrder, setSortOrder] = createSignal<SortOrder>(
1592
+ String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc"
1593
+ )
1594
+ const [scrollMode, setScrollMode] = createSignal<ScrollMode>(
1595
+ String(api.kv.get(`${KV_PREFIX}.scroll_mode`, "wheel")) === "click" ? "click" : "wheel"
1596
+ )
1475
1597
 
1476
- const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sessionId: "" }
1598
+ const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, sessionId: "" }
1477
1599
 
1478
1600
  api.slots.register(createSidebarSlot(api, signals))
1479
1601
 
@@ -1505,6 +1627,60 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1505
1627
  ))
1506
1628
  },
1507
1629
  },
1630
+ {
1631
+ title: "SubAgent Magazine: Sort Order",
1632
+ value: "subagent-order",
1633
+ description: "Set sub-agent entry sort order (desc / asc)",
1634
+ slash: { name: "subagent-order" },
1635
+ onSelect: (dialog) => {
1636
+ const t = (k: string) => I18N[lang()][k] ?? k
1637
+ dialog?.replace(() => (
1638
+ <api.ui.DialogSelect
1639
+ title="Sort Order / 排序方式"
1640
+ options={[
1641
+ { title: t("order.desc"), value: "desc" },
1642
+ { title: t("order.asc"), value: "asc" },
1643
+ ]}
1644
+ onSelect={(opt) => {
1645
+ const o = opt.value as SortOrder
1646
+ setSortOrder(o)
1647
+ api.kv.set(`${KV_PREFIX}.order`, o)
1648
+ api.ui.toast({
1649
+ message: o === "desc" ? t("order.desc") : t("order.asc"),
1650
+ })
1651
+ dialog?.clear()
1652
+ }}
1653
+ />
1654
+ ))
1655
+ },
1656
+ },
1657
+ {
1658
+ title: "SubAgent Magazine: Scroll Mode",
1659
+ value: "subagent-scroll",
1660
+ description: "Set scroll mode (wheel / click)",
1661
+ slash: { name: "subagent-scroll" },
1662
+ onSelect: (dialog) => {
1663
+ const t = (k: string) => I18N[lang()][k] ?? k
1664
+ dialog?.replace(() => (
1665
+ <api.ui.DialogSelect
1666
+ title="Scroll Mode / 滚动模式"
1667
+ options={[
1668
+ { title: t("scroll.wheel"), value: "wheel" },
1669
+ { title: t("scroll.click"), value: "click" },
1670
+ ]}
1671
+ onSelect={(opt) => {
1672
+ const m = opt.value as ScrollMode
1673
+ setScrollMode(m)
1674
+ api.kv.set(`${KV_PREFIX}.scroll_mode`, m)
1675
+ api.ui.toast({
1676
+ message: m === "wheel" ? t("scroll.wheel") : t("scroll.click"),
1677
+ })
1678
+ dialog?.clear()
1679
+ }}
1680
+ />
1681
+ ))
1682
+ },
1683
+ },
1508
1684
  {
1509
1685
  title: "SubAgent Magazine: Max Entries",
1510
1686
  value: "subagent-max",