opencode-subagent-magazine 1.1.3 → 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.3",
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",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.1.3";
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
 
@@ -255,6 +267,8 @@ function SubAgentPanel(props: {
255
267
  api: TuiPluginApi
256
268
  lang: () => Lang
257
269
  maxEntries: () => number
270
+ sortOrder: () => SortOrder
271
+ scrollMode: () => ScrollMode
258
272
  sessionId: string
259
273
  }): JSX.Element {
260
274
  const t = (key: string) => I18N[props.lang()][key] ?? key
@@ -385,6 +399,8 @@ function SubAgentPanel(props: {
385
399
  const [hoveredOpen, setHoveredOpen] = createSignal<string | undefined>(undefined)
386
400
  const [hoveredDismiss, setHoveredDismiss] = createSignal<string | undefined>(undefined)
387
401
  const [hoveredTop, setHoveredTop] = createSignal(false)
402
+ const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false)
403
+ const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false)
388
404
  const [scrollOffset, setScrollOffset] = createSignal(
389
405
  (() => { try { return loadSessionData()[props.sessionId]?.scroll ?? 0 } catch { return 0 } })()
390
406
  )
@@ -1020,7 +1036,11 @@ function SubAgentPanel(props: {
1020
1036
  // ── derived signals ──
1021
1037
  // Stable list — only changes when entryMap changes
1022
1038
  const entryList = createMemo(() => {
1023
- 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)
1024
1044
  })
1025
1045
 
1026
1046
  const max = props.maxEntries
@@ -1034,6 +1054,48 @@ function SubAgentPanel(props: {
1034
1054
  const hiddenAbove = createMemo(() => clampedOffset())
1035
1055
  const hiddenBelow = createMemo(() => Math.max(0, entryList().length - clampedOffset() - max()))
1036
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
+
1037
1099
  const entries = createMemo(() => {
1038
1100
  const nowVal = now()
1039
1101
  return entryList().map((e) => ({
@@ -1193,6 +1255,7 @@ function SubAgentPanel(props: {
1193
1255
  >
1194
1256
  <box
1195
1257
  onMouseScroll={(e) => {
1258
+ if (props.scrollMode() === "click") return
1196
1259
  const total = entryList().length
1197
1260
  const m = max()
1198
1261
  if (total <= m) return
@@ -1205,8 +1268,28 @@ function SubAgentPanel(props: {
1205
1268
  }}
1206
1269
  >
1207
1270
  <Show when={hiddenAbove() > 0}>
1208
- <text style={{ fg: pal().muted }}>
1209
- {" "}&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>
1210
1293
  </text>
1211
1294
  </Show>
1212
1295
  <For each={visibleList()}>
@@ -1394,23 +1477,52 @@ function SubAgentPanel(props: {
1394
1477
  )
1395
1478
  }}
1396
1479
  </For>
1397
- <Show when={hiddenBelow() > 0 || scrollOffset() > 0}>
1480
+ <Show when={hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max())}>
1398
1481
  {(() => {
1399
1482
  const showMore = hiddenBelow() > 0
1400
- const showTop = scrollOffset() > 0
1483
+ const showTop = props.sortOrder() === "desc"
1484
+ ? scrollOffset() > 0
1485
+ : entryList().length > max() && clampedOffset() < entryList().length - max()
1401
1486
  const left = showMore ? ` \u2193 ${hiddenBelow()} ${t("scroll.more")}` : " "
1402
- const right = `\u2191 ${t("scroll.top")}`
1487
+ const right = props.sortOrder() === "desc"
1488
+ ? `\u2191 ${t("scroll.top")}`
1489
+ : `\u2193 ${t("scroll.bottom")}`
1403
1490
  const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0
1404
1491
  return (
1405
1492
  <box flexDirection="row">
1406
- <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>
1407
1510
  {showTop ? (
1408
1511
  <>
1409
1512
  <text style={{ fg: pal().muted }}>{" ".repeat(pad)}</text>
1410
1513
  <text
1411
1514
  onMouseOver={() => setHoveredTop(true)}
1412
1515
  onMouseOut={() => setHoveredTop(false)}
1413
- 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
+ }}
1414
1526
  >
1415
1527
  <span style={{ fg: hoveredTop() ? pal().warning : pal().muted }}>{right}</span>
1416
1528
  </text>
@@ -1436,6 +1548,10 @@ interface SharedSignals {
1436
1548
  setLang: (l: Lang) => void
1437
1549
  maxEntries: () => number
1438
1550
  setMaxEntries: (n: number) => void
1551
+ sortOrder: () => SortOrder
1552
+ setSortOrder: (o: SortOrder) => void
1553
+ scrollMode: () => ScrollMode
1554
+ setScrollMode: (m: ScrollMode) => void
1439
1555
  sessionId: string
1440
1556
  }
1441
1557
 
@@ -1451,6 +1567,8 @@ function createSidebarSlot(api: TuiPluginApi, sig: SharedSignals): TuiSlotPlugin
1451
1567
  api={api}
1452
1568
  lang={sig.lang}
1453
1569
  maxEntries={sig.maxEntries}
1570
+ sortOrder={sig.sortOrder}
1571
+ scrollMode={sig.scrollMode}
1454
1572
  sessionId={input.session_id}
1455
1573
  />
1456
1574
  )
@@ -1470,8 +1588,14 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1470
1588
  const [maxEntries, setMaxEntries] = createSignal(
1471
1589
  parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
1472
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
+ )
1473
1597
 
1474
- const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sessionId: "" }
1598
+ const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, sessionId: "" }
1475
1599
 
1476
1600
  api.slots.register(createSidebarSlot(api, signals))
1477
1601
 
@@ -1503,6 +1627,60 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1503
1627
  ))
1504
1628
  },
1505
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
+ },
1506
1684
  {
1507
1685
  title: "SubAgent Magazine: Max Entries",
1508
1686
  value: "subagent-max",