opencode-agents-monitor 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dqz00116
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source srcset="assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
4
+ <source srcset="assets/logo-light.svg" media="(prefers-color-scheme: light)">
5
+ <img src="assets/logo-light.svg" alt="agents logo">
6
+ </picture>
7
+ </p>
8
+ <p align="center">Live sub-agent status in the OpenCode sidebar.</p>
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/opencode-agents-monitor"><img alt="npm" src="https://img.shields.io/npm/v/opencode-agents-monitor?style=flat-square" /></a>
11
+ <a href="https://github.com/Dqz00116/opencode-agents-monitor/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" /></a>
12
+ </p>
13
+ <p align="center">
14
+ English | <a href="README.zh-CN.md">中文</a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ An [OpenCode](https://opencode.ai) TUI plugin that adds an **Agents** widget to the session
20
+ sidebar, so you can watch every sub-agent your session spawns — what it's doing, which
21
+ model it runs on, how much context it has burned, and how long it has been at it.
22
+
23
+ **What makes it different:** other sidebar plugins *list* your agents — this one shows you **what they are doing right now**. Live `session.status` events drive five states (`thinking` / `tool` / `retry` / `done` / `idle`), the **current tool call** is visible while it is still running, and finished agents from before the TUI started are **hydrated** from the API so context and elapsed time are never lost.
24
+
25
+ <p align="center">
26
+ <img src="assets/opencode-agents-monitor.gif" alt="opencode-agents-monitor demo">
27
+ </p>
28
+
29
+ ### Features
30
+
31
+ - **Live status per sub-agent** — `thinking` / `tool` / `retry` / `done` / `idle`,
32
+ driven by the server's `session.status` event stream
33
+ - **Current tool call** — see `bash npm test` while it's still running
34
+ - **Context & cost** — token usage and spend per agent
35
+ - **Elapsed timer** — starts when the agent goes busy, freezes when it's done
36
+ - **Auto-archive** — finished agents fold into `Archived (n)`, sorted by completion time
37
+ - **Click to expand** — rows collapse to a one-line summary by default
38
+ - **Jump into the agent** — the `[view]` button opens the agent's own session view
39
+ (same as the built-in "Go to child session"); press `up` to come back
40
+ - **History hydration** — agents from before the current TUI was started still get
41
+ `ctx` / `elapsed`, fetched lazily from the API
42
+ - **Flicker-free** — fixed-height rows and a fixed-width layout that fits the sidebar
43
+
44
+ ### Installation
45
+
46
+ ```bash
47
+ opencode plugin opencode-agents-monitor
48
+ ```
49
+
50
+ Restart OpenCode afterwards. The widget lives in the session sidebar —
51
+ press `ctrl+x` then `b` if the sidebar is hidden.
52
+
53
+ <details>
54
+ <summary>Manual installation</summary>
55
+
56
+ Add to `~/.config/opencode/tui.json` (global) or `.opencode/tui.json` (project):
57
+
58
+ ```json
59
+ {
60
+ "plugin": ["opencode-agents-monitor"]
61
+ }
62
+ ```
63
+
64
+ </details>
65
+
66
+ ### Usage
67
+
68
+ | Action | Result |
69
+ | --- | --- |
70
+ | Click `Agents` header | Collapse / expand the whole widget (persists) |
71
+ | Click an agent row | Expand details / collapse to one-line summary |
72
+ | Click `[view]` | Open that agent's session (messages, tools, everything) |
73
+ | Click `Archived (n)` | Show / hide finished agents (persists) |
74
+
75
+ Status markers: `*` active · `!` retrying · `-` idle / done.
76
+
77
+ ### How it works
78
+
79
+ - Registers into the TUI's `sidebar_content` slot — the same extension point the
80
+ built-in Context / Todo widgets use. No patched host code.
81
+ - Tracks child sessions (`parentID`) via `session.created` / `session.updated` /
82
+ `session.deleted` events plus one initial `session.list()`.
83
+ - Live state comes from `session.status` events (`busy` / `retry` / `idle`) and the
84
+ shared message store; the current tool is the last `tool` part of the latest
85
+ assistant message.
86
+ - Finished agents are hydrated once via `session.messages` so historical sessions
87
+ still report context size and elapsed time.
88
+ - All widget state lives outside the slot component tree, so the slot registry's
89
+ renderer re-invocation never loses state.
90
+
91
+ ### Development
92
+
93
+ ```bash
94
+ git clone https://github.com/Dqz00116/opencode-agents-monitor
95
+ cd opencode-agents-monitor
96
+ bun install
97
+ ```
98
+
99
+ Add it to your `tui.json` as a file plugin while hacking:
100
+
101
+ ```json
102
+ {
103
+ "plugin": ["./path/to/opencode-agents-monitor/src/index.tsx"]
104
+ }
105
+ ```
106
+
107
+ The logo is generated: `node script/logo.mjs` (pixel wordmark in the opencode
108
+ ornate style).
109
+
110
+ ### License
111
+
112
+ MIT
@@ -0,0 +1,109 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source srcset="assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
4
+ <source srcset="assets/logo-light.svg" media="(prefers-color-scheme: light)">
5
+ <img src="assets/logo-light.svg" alt="agents logo">
6
+ </picture>
7
+ </p>
8
+ <p align="center">在 OpenCode 侧边栏实时观察子代理的运行状态。</p>
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/opencode-agents-monitor"><img alt="npm" src="https://img.shields.io/npm/v/opencode-agents-monitor?style=flat-square" /></a>
11
+ <a href="https://github.com/Dqz00116/opencode-agents-monitor/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" /></a>
12
+ </p>
13
+ <p align="center">
14
+ <a href="README.md">English</a> | 中文
15
+ </p>
16
+
17
+ ---
18
+
19
+ 一个 [OpenCode](https://opencode.ai) TUI 插件,在会话侧边栏添加 **Agents** 组件,
20
+ 让你实时掌握当前会话派生的每个子代理:它在做什么、用哪个模型、
21
+ 烧了多少上下文、已经跑了多久。
22
+
23
+ **它与众不同之处:** 其他侧边栏插件只是*列出*你的代理——这个插件展示的是它们**此刻正在做什么**。实时的 `session.status` 事件驱动五种状态(`thinking` / `tool` / `retry` / `done` / `idle`),**当前工具调用**在运行期间即可实时可见,而 TUI 启动之前就已结束的代理也会通过 API **水合(hydrated)**,因此上下文与耗时信息永不丢失。
24
+
25
+ <p align="center">
26
+ <img src="assets/opencode-agents-monitor.gif" alt="opencode-agents-monitor 演示">
27
+ </p>
28
+
29
+ ### 特性
30
+
31
+ - **实时状态** — `thinking` / `tool` / `retry` / `done` / `idle`,
32
+ 由服务端 `session.status` 事件流驱动
33
+ - **当前工具调用** — 工具还在跑就能看到,如 `bash npm test`
34
+ - **上下文与花费** — 每个代理的 token 用量和成本
35
+ - **耗时计时** — 代理进入 busy 开始计时,完成后冻结
36
+ - **自动归档** — 已完成的代理折叠进 `Archived (n)`,按完成时间倒序
37
+ - **点击展开** — 行默认折叠为单行概况,点击查看详情
38
+ - **跳进代理会话** — `[view]` 按钮直达该代理自己的会话视图
39
+ (等同内置的 "Go to child session"),按 `up` 返回
40
+ - **历史数据水合** — 重启 TUI 之前的代理也能显示 `ctx` / `elapsed`,
41
+ 通过 API 惰性拉取一次
42
+ - **稳定不闪动** — 定高行 + 适配侧栏宽度的定宽布局
43
+
44
+ ### 安装
45
+
46
+ ```bash
47
+ opencode plugin opencode-agents-monitor
48
+ ```
49
+
50
+ 安装后重启 OpenCode。组件位于会话侧边栏——
51
+ 如果侧栏处于隐藏状态,按 `ctrl+x` 再按 `b` 打开。
52
+
53
+ <details>
54
+ <summary>手动安装</summary>
55
+
56
+ 在 `~/.config/opencode/tui.json`(全局)或 `.opencode/tui.json`(项目级)中添加:
57
+
58
+ ```json
59
+ {
60
+ "plugin": ["opencode-agents-monitor"]
61
+ }
62
+ ```
63
+
64
+ </details>
65
+
66
+ ### 使用
67
+
68
+ | 操作 | 效果 |
69
+ | --- | --- |
70
+ | 点击 `Agents` 标题 | 折叠 / 展开整个组件(状态持久化) |
71
+ | 点击代理行 | 展开详情 / 折叠为单行概况 |
72
+ | 点击 `[view]` | 打开该代理的会话(消息、工具调用全过程) |
73
+ | 点击 `Archived (n)` | 显示 / 隐藏已完成的代理(状态持久化) |
74
+
75
+ 状态标记:`*` 活动中 · `!` 重试中 · `-` 空闲 / 已完成。
76
+
77
+ ### 工作原理
78
+
79
+ - 注册到 TUI 的 `sidebar_content` 插槽——与内置 Context / Todo 组件
80
+ 相同的扩展点,无需修改宿主代码
81
+ - 通过 `session.created` / `session.updated` / `session.deleted` 事件
82
+ 及一次初始 `session.list()` 追踪子会话(`parentID` 关联)
83
+ - 实时状态来自 `session.status` 事件(`busy` / `retry` / `idle`)与共享
84
+ 消息存储;当前工具取最新一条 assistant 消息的最后一个 `tool` part
85
+ - 已结束的代理通过 `session.messages` 一次性水合,历史会话也能报告
86
+ 上下文大小与耗时
87
+ - 组件全部状态保存在插槽组件树之外,插槽注册表重渲染不会丢失状态
88
+
89
+ ### 开发
90
+
91
+ ```bash
92
+ git clone https://github.com/Dqz00116/opencode-agents-monitor
93
+ cd opencode-agents-monitor
94
+ bun install
95
+ ```
96
+
97
+ 开发时在 `tui.json` 中以文件插件方式引用:
98
+
99
+ ```json
100
+ {
101
+ "plugin": ["./path/to/opencode-agents-monitor/src/index.tsx"]
102
+ }
103
+ ```
104
+
105
+ Logo 由脚本生成:`node script/logo.mjs`(opencode ornate 风格的像素字标)。
106
+
107
+ ### 许可证
108
+
109
+ MIT
@@ -0,0 +1,110 @@
1
+ <svg width="174" height="48" viewBox="0 0 174 48" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="0" y="6" width="6" height="6" fill="#F1ECEC"/>
3
+ <rect x="6" y="6" width="6" height="6" fill="#F1ECEC"/>
4
+ <rect x="12" y="6" width="6" height="6" fill="#F1ECEC"/>
5
+ <rect x="18" y="6" width="6" height="6" fill="#F1ECEC"/>
6
+ <rect x="0" y="12" width="6" height="6" fill="#F1ECEC"/>
7
+ <rect x="18" y="12" width="6" height="6" fill="#F1ECEC"/>
8
+ <rect x="0" y="18" width="6" height="6" fill="#F1ECEC"/>
9
+ <rect x="6" y="18" width="6" height="6" fill="#F1ECEC"/>
10
+ <rect x="12" y="18" width="6" height="6" fill="#F1ECEC"/>
11
+ <rect x="18" y="18" width="6" height="6" fill="#F1ECEC"/>
12
+ <rect x="0" y="24" width="6" height="6" fill="#F1ECEC"/>
13
+ <rect x="6" y="24" width="6" height="6" fill="#4B4646"/>
14
+ <rect x="12" y="24" width="6" height="6" fill="#4B4646"/>
15
+ <rect x="18" y="24" width="6" height="6" fill="#F1ECEC"/>
16
+ <rect x="0" y="30" width="6" height="6" fill="#F1ECEC"/>
17
+ <rect x="6" y="30" width="6" height="6" fill="#4B4646"/>
18
+ <rect x="12" y="30" width="6" height="6" fill="#4B4646"/>
19
+ <rect x="18" y="30" width="6" height="6" fill="#F1ECEC"/>
20
+ <rect x="30" y="6" width="6" height="6" fill="#F1ECEC"/>
21
+ <rect x="36" y="6" width="6" height="6" fill="#F1ECEC"/>
22
+ <rect x="42" y="6" width="6" height="6" fill="#F1ECEC"/>
23
+ <rect x="48" y="6" width="6" height="6" fill="#F1ECEC"/>
24
+ <rect x="30" y="12" width="6" height="6" fill="#F1ECEC"/>
25
+ <rect x="48" y="12" width="6" height="6" fill="#F1ECEC"/>
26
+ <rect x="30" y="18" width="6" height="6" fill="#F1ECEC"/>
27
+ <rect x="36" y="18" width="6" height="6" fill="#4B4646"/>
28
+ <rect x="42" y="18" width="6" height="6" fill="#4B4646"/>
29
+ <rect x="48" y="18" width="6" height="6" fill="#F1ECEC"/>
30
+ <rect x="30" y="24" width="6" height="6" fill="#F1ECEC"/>
31
+ <rect x="36" y="24" width="6" height="6" fill="#4B4646"/>
32
+ <rect x="42" y="24" width="6" height="6" fill="#4B4646"/>
33
+ <rect x="48" y="24" width="6" height="6" fill="#F1ECEC"/>
34
+ <rect x="30" y="30" width="6" height="6" fill="#F1ECEC"/>
35
+ <rect x="36" y="30" width="6" height="6" fill="#F1ECEC"/>
36
+ <rect x="42" y="30" width="6" height="6" fill="#F1ECEC"/>
37
+ <rect x="48" y="30" width="6" height="6" fill="#F1ECEC"/>
38
+ <rect x="48" y="36" width="6" height="6" fill="#F1ECEC"/>
39
+ <rect x="30" y="42" width="6" height="6" fill="#F1ECEC"/>
40
+ <rect x="36" y="42" width="6" height="6" fill="#F1ECEC"/>
41
+ <rect x="42" y="42" width="6" height="6" fill="#F1ECEC"/>
42
+ <rect x="48" y="42" width="6" height="6" fill="#F1ECEC"/>
43
+ <rect x="60" y="6" width="6" height="6" fill="#F1ECEC"/>
44
+ <rect x="66" y="6" width="6" height="6" fill="#F1ECEC"/>
45
+ <rect x="72" y="6" width="6" height="6" fill="#F1ECEC"/>
46
+ <rect x="78" y="6" width="6" height="6" fill="#F1ECEC"/>
47
+ <rect x="60" y="12" width="6" height="6" fill="#F1ECEC"/>
48
+ <rect x="78" y="12" width="6" height="6" fill="#F1ECEC"/>
49
+ <rect x="60" y="18" width="6" height="6" fill="#F1ECEC"/>
50
+ <rect x="66" y="18" width="6" height="6" fill="#F1ECEC"/>
51
+ <rect x="72" y="18" width="6" height="6" fill="#F1ECEC"/>
52
+ <rect x="78" y="18" width="6" height="6" fill="#F1ECEC"/>
53
+ <rect x="60" y="24" width="6" height="6" fill="#F1ECEC"/>
54
+ <rect x="66" y="24" width="6" height="6" fill="#4B4646"/>
55
+ <rect x="72" y="24" width="6" height="6" fill="#4B4646"/>
56
+ <rect x="78" y="24" width="6" height="6" fill="#4B4646"/>
57
+ <rect x="60" y="30" width="6" height="6" fill="#F1ECEC"/>
58
+ <rect x="66" y="30" width="6" height="6" fill="#F1ECEC"/>
59
+ <rect x="72" y="30" width="6" height="6" fill="#F1ECEC"/>
60
+ <rect x="78" y="30" width="6" height="6" fill="#F1ECEC"/>
61
+ <rect x="90" y="6" width="6" height="6" fill="#F1ECEC"/>
62
+ <rect x="96" y="6" width="6" height="6" fill="#F1ECEC"/>
63
+ <rect x="102" y="6" width="6" height="6" fill="#F1ECEC"/>
64
+ <rect x="108" y="6" width="6" height="6" fill="#F1ECEC"/>
65
+ <rect x="90" y="12" width="6" height="6" fill="#F1ECEC"/>
66
+ <rect x="108" y="12" width="6" height="6" fill="#F1ECEC"/>
67
+ <rect x="90" y="18" width="6" height="6" fill="#F1ECEC"/>
68
+ <rect x="96" y="18" width="6" height="6" fill="#4B4646"/>
69
+ <rect x="102" y="18" width="6" height="6" fill="#4B4646"/>
70
+ <rect x="108" y="18" width="6" height="6" fill="#F1ECEC"/>
71
+ <rect x="90" y="24" width="6" height="6" fill="#F1ECEC"/>
72
+ <rect x="96" y="24" width="6" height="6" fill="#4B4646"/>
73
+ <rect x="102" y="24" width="6" height="6" fill="#4B4646"/>
74
+ <rect x="108" y="24" width="6" height="6" fill="#F1ECEC"/>
75
+ <rect x="90" y="30" width="6" height="6" fill="#F1ECEC"/>
76
+ <rect x="96" y="30" width="6" height="6" fill="#4B4646"/>
77
+ <rect x="102" y="30" width="6" height="6" fill="#4B4646"/>
78
+ <rect x="108" y="30" width="6" height="6" fill="#F1ECEC"/>
79
+ <rect x="126" y="6" width="6" height="6" fill="#F1ECEC"/>
80
+ <rect x="120" y="12" width="6" height="6" fill="#F1ECEC"/>
81
+ <rect x="126" y="12" width="6" height="6" fill="#F1ECEC"/>
82
+ <rect x="132" y="12" width="6" height="6" fill="#F1ECEC"/>
83
+ <rect x="138" y="12" width="6" height="6" fill="#F1ECEC"/>
84
+ <rect x="126" y="18" width="6" height="6" fill="#F1ECEC"/>
85
+ <rect x="132" y="18" width="6" height="6" fill="#4B4646"/>
86
+ <rect x="138" y="18" width="6" height="6" fill="#4B4646"/>
87
+ <rect x="126" y="24" width="6" height="6" fill="#F1ECEC"/>
88
+ <rect x="132" y="24" width="6" height="6" fill="#4B4646"/>
89
+ <rect x="138" y="24" width="6" height="6" fill="#4B4646"/>
90
+ <rect x="126" y="30" width="6" height="6" fill="#F1ECEC"/>
91
+ <rect x="132" y="30" width="6" height="6" fill="#F1ECEC"/>
92
+ <rect x="138" y="30" width="6" height="6" fill="#F1ECEC"/>
93
+ <rect x="150" y="6" width="6" height="6" fill="#F1ECEC"/>
94
+ <rect x="156" y="6" width="6" height="6" fill="#F1ECEC"/>
95
+ <rect x="162" y="6" width="6" height="6" fill="#F1ECEC"/>
96
+ <rect x="168" y="6" width="6" height="6" fill="#F1ECEC"/>
97
+ <rect x="150" y="12" width="6" height="6" fill="#F1ECEC"/>
98
+ <rect x="150" y="18" width="6" height="6" fill="#F1ECEC"/>
99
+ <rect x="156" y="18" width="6" height="6" fill="#F1ECEC"/>
100
+ <rect x="162" y="18" width="6" height="6" fill="#F1ECEC"/>
101
+ <rect x="168" y="18" width="6" height="6" fill="#F1ECEC"/>
102
+ <rect x="150" y="24" width="6" height="6" fill="#4B4646"/>
103
+ <rect x="156" y="24" width="6" height="6" fill="#4B4646"/>
104
+ <rect x="162" y="24" width="6" height="6" fill="#4B4646"/>
105
+ <rect x="168" y="24" width="6" height="6" fill="#F1ECEC"/>
106
+ <rect x="150" y="30" width="6" height="6" fill="#F1ECEC"/>
107
+ <rect x="156" y="30" width="6" height="6" fill="#F1ECEC"/>
108
+ <rect x="162" y="30" width="6" height="6" fill="#F1ECEC"/>
109
+ <rect x="168" y="30" width="6" height="6" fill="#F1ECEC"/>
110
+ </svg>
@@ -0,0 +1,110 @@
1
+ <svg width="174" height="48" viewBox="0 0 174 48" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="0" y="6" width="6" height="6" fill="#211E1E"/>
3
+ <rect x="6" y="6" width="6" height="6" fill="#211E1E"/>
4
+ <rect x="12" y="6" width="6" height="6" fill="#211E1E"/>
5
+ <rect x="18" y="6" width="6" height="6" fill="#211E1E"/>
6
+ <rect x="0" y="12" width="6" height="6" fill="#211E1E"/>
7
+ <rect x="18" y="12" width="6" height="6" fill="#211E1E"/>
8
+ <rect x="0" y="18" width="6" height="6" fill="#211E1E"/>
9
+ <rect x="6" y="18" width="6" height="6" fill="#211E1E"/>
10
+ <rect x="12" y="18" width="6" height="6" fill="#211E1E"/>
11
+ <rect x="18" y="18" width="6" height="6" fill="#211E1E"/>
12
+ <rect x="0" y="24" width="6" height="6" fill="#211E1E"/>
13
+ <rect x="6" y="24" width="6" height="6" fill="#CFCECD"/>
14
+ <rect x="12" y="24" width="6" height="6" fill="#CFCECD"/>
15
+ <rect x="18" y="24" width="6" height="6" fill="#211E1E"/>
16
+ <rect x="0" y="30" width="6" height="6" fill="#211E1E"/>
17
+ <rect x="6" y="30" width="6" height="6" fill="#CFCECD"/>
18
+ <rect x="12" y="30" width="6" height="6" fill="#CFCECD"/>
19
+ <rect x="18" y="30" width="6" height="6" fill="#211E1E"/>
20
+ <rect x="30" y="6" width="6" height="6" fill="#211E1E"/>
21
+ <rect x="36" y="6" width="6" height="6" fill="#211E1E"/>
22
+ <rect x="42" y="6" width="6" height="6" fill="#211E1E"/>
23
+ <rect x="48" y="6" width="6" height="6" fill="#211E1E"/>
24
+ <rect x="30" y="12" width="6" height="6" fill="#211E1E"/>
25
+ <rect x="48" y="12" width="6" height="6" fill="#211E1E"/>
26
+ <rect x="30" y="18" width="6" height="6" fill="#211E1E"/>
27
+ <rect x="36" y="18" width="6" height="6" fill="#CFCECD"/>
28
+ <rect x="42" y="18" width="6" height="6" fill="#CFCECD"/>
29
+ <rect x="48" y="18" width="6" height="6" fill="#211E1E"/>
30
+ <rect x="30" y="24" width="6" height="6" fill="#211E1E"/>
31
+ <rect x="36" y="24" width="6" height="6" fill="#CFCECD"/>
32
+ <rect x="42" y="24" width="6" height="6" fill="#CFCECD"/>
33
+ <rect x="48" y="24" width="6" height="6" fill="#211E1E"/>
34
+ <rect x="30" y="30" width="6" height="6" fill="#211E1E"/>
35
+ <rect x="36" y="30" width="6" height="6" fill="#211E1E"/>
36
+ <rect x="42" y="30" width="6" height="6" fill="#211E1E"/>
37
+ <rect x="48" y="30" width="6" height="6" fill="#211E1E"/>
38
+ <rect x="48" y="36" width="6" height="6" fill="#211E1E"/>
39
+ <rect x="30" y="42" width="6" height="6" fill="#211E1E"/>
40
+ <rect x="36" y="42" width="6" height="6" fill="#211E1E"/>
41
+ <rect x="42" y="42" width="6" height="6" fill="#211E1E"/>
42
+ <rect x="48" y="42" width="6" height="6" fill="#211E1E"/>
43
+ <rect x="60" y="6" width="6" height="6" fill="#211E1E"/>
44
+ <rect x="66" y="6" width="6" height="6" fill="#211E1E"/>
45
+ <rect x="72" y="6" width="6" height="6" fill="#211E1E"/>
46
+ <rect x="78" y="6" width="6" height="6" fill="#211E1E"/>
47
+ <rect x="60" y="12" width="6" height="6" fill="#211E1E"/>
48
+ <rect x="78" y="12" width="6" height="6" fill="#211E1E"/>
49
+ <rect x="60" y="18" width="6" height="6" fill="#211E1E"/>
50
+ <rect x="66" y="18" width="6" height="6" fill="#211E1E"/>
51
+ <rect x="72" y="18" width="6" height="6" fill="#211E1E"/>
52
+ <rect x="78" y="18" width="6" height="6" fill="#211E1E"/>
53
+ <rect x="60" y="24" width="6" height="6" fill="#211E1E"/>
54
+ <rect x="66" y="24" width="6" height="6" fill="#CFCECD"/>
55
+ <rect x="72" y="24" width="6" height="6" fill="#CFCECD"/>
56
+ <rect x="78" y="24" width="6" height="6" fill="#CFCECD"/>
57
+ <rect x="60" y="30" width="6" height="6" fill="#211E1E"/>
58
+ <rect x="66" y="30" width="6" height="6" fill="#211E1E"/>
59
+ <rect x="72" y="30" width="6" height="6" fill="#211E1E"/>
60
+ <rect x="78" y="30" width="6" height="6" fill="#211E1E"/>
61
+ <rect x="90" y="6" width="6" height="6" fill="#211E1E"/>
62
+ <rect x="96" y="6" width="6" height="6" fill="#211E1E"/>
63
+ <rect x="102" y="6" width="6" height="6" fill="#211E1E"/>
64
+ <rect x="108" y="6" width="6" height="6" fill="#211E1E"/>
65
+ <rect x="90" y="12" width="6" height="6" fill="#211E1E"/>
66
+ <rect x="108" y="12" width="6" height="6" fill="#211E1E"/>
67
+ <rect x="90" y="18" width="6" height="6" fill="#211E1E"/>
68
+ <rect x="96" y="18" width="6" height="6" fill="#CFCECD"/>
69
+ <rect x="102" y="18" width="6" height="6" fill="#CFCECD"/>
70
+ <rect x="108" y="18" width="6" height="6" fill="#211E1E"/>
71
+ <rect x="90" y="24" width="6" height="6" fill="#211E1E"/>
72
+ <rect x="96" y="24" width="6" height="6" fill="#CFCECD"/>
73
+ <rect x="102" y="24" width="6" height="6" fill="#CFCECD"/>
74
+ <rect x="108" y="24" width="6" height="6" fill="#211E1E"/>
75
+ <rect x="90" y="30" width="6" height="6" fill="#211E1E"/>
76
+ <rect x="96" y="30" width="6" height="6" fill="#CFCECD"/>
77
+ <rect x="102" y="30" width="6" height="6" fill="#CFCECD"/>
78
+ <rect x="108" y="30" width="6" height="6" fill="#211E1E"/>
79
+ <rect x="126" y="6" width="6" height="6" fill="#211E1E"/>
80
+ <rect x="120" y="12" width="6" height="6" fill="#211E1E"/>
81
+ <rect x="126" y="12" width="6" height="6" fill="#211E1E"/>
82
+ <rect x="132" y="12" width="6" height="6" fill="#211E1E"/>
83
+ <rect x="138" y="12" width="6" height="6" fill="#211E1E"/>
84
+ <rect x="126" y="18" width="6" height="6" fill="#211E1E"/>
85
+ <rect x="132" y="18" width="6" height="6" fill="#CFCECD"/>
86
+ <rect x="138" y="18" width="6" height="6" fill="#CFCECD"/>
87
+ <rect x="126" y="24" width="6" height="6" fill="#211E1E"/>
88
+ <rect x="132" y="24" width="6" height="6" fill="#CFCECD"/>
89
+ <rect x="138" y="24" width="6" height="6" fill="#CFCECD"/>
90
+ <rect x="126" y="30" width="6" height="6" fill="#211E1E"/>
91
+ <rect x="132" y="30" width="6" height="6" fill="#211E1E"/>
92
+ <rect x="138" y="30" width="6" height="6" fill="#211E1E"/>
93
+ <rect x="150" y="6" width="6" height="6" fill="#211E1E"/>
94
+ <rect x="156" y="6" width="6" height="6" fill="#211E1E"/>
95
+ <rect x="162" y="6" width="6" height="6" fill="#211E1E"/>
96
+ <rect x="168" y="6" width="6" height="6" fill="#211E1E"/>
97
+ <rect x="150" y="12" width="6" height="6" fill="#211E1E"/>
98
+ <rect x="150" y="18" width="6" height="6" fill="#211E1E"/>
99
+ <rect x="156" y="18" width="6" height="6" fill="#211E1E"/>
100
+ <rect x="162" y="18" width="6" height="6" fill="#211E1E"/>
101
+ <rect x="168" y="18" width="6" height="6" fill="#211E1E"/>
102
+ <rect x="150" y="24" width="6" height="6" fill="#CFCECD"/>
103
+ <rect x="156" y="24" width="6" height="6" fill="#CFCECD"/>
104
+ <rect x="162" y="24" width="6" height="6" fill="#CFCECD"/>
105
+ <rect x="168" y="24" width="6" height="6" fill="#211E1E"/>
106
+ <rect x="150" y="30" width="6" height="6" fill="#211E1E"/>
107
+ <rect x="156" y="30" width="6" height="6" fill="#211E1E"/>
108
+ <rect x="162" y="30" width="6" height="6" fill="#211E1E"/>
109
+ <rect x="168" y="30" width="6" height="6" fill="#211E1E"/>
110
+ </svg>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "opencode-agents-monitor",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode TUI plugin — live sub-agent status in the sidebar",
5
+ "author": "Dqz00116",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Dqz00116/opencode-agents-monitor.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Dqz00116/opencode-agents-monitor/issues"
13
+ },
14
+ "homepage": "https://github.com/Dqz00116/opencode-agents-monitor",
15
+ "keywords": [
16
+ "opencode",
17
+ "opencode-plugin",
18
+ "tui",
19
+ "subagent",
20
+ "sidebar"
21
+ ],
22
+ "engines": {
23
+ "opencode": ">=1.18.0"
24
+ },
25
+ "exports": {
26
+ "./tui": "./src/index.tsx"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "assets"
31
+ ],
32
+ "dependencies": {
33
+ "@opencode-ai/plugin": "^1.18.1",
34
+ "solid-js": "^1.9.12",
35
+ "@opentui/solid": "^0.3.4"
36
+ }
37
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,354 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
+ import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
4
+ import { createEffect, createMemo, createRoot, createSignal, For, Show } from "solid-js"
5
+
6
+ type Run = { start: number; end?: number }
7
+
8
+ const KV_COLLAPSED = "agents_sidebar.collapsed"
9
+ const KV_ARCHIVED = "agents_sidebar.show_archived"
10
+
11
+ const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
12
+
13
+ type Tracker = {
14
+ active: (parentID: string) => Session[]
15
+ archived: (parentID: string) => Session[]
16
+ run: (sessionID: string) => Run | undefined
17
+ history: (sessionID: string) => History | undefined
18
+ now: () => number
19
+ collapsed: () => boolean
20
+ toggleCollapsed: () => void
21
+ showArchived: () => boolean
22
+ toggleArchived: () => void
23
+ expanded: (sessionID: string, fallback: boolean) => boolean
24
+ toggleExpanded: (sessionID: string, fallback: boolean) => void
25
+ }
26
+
27
+ type History = { ctx?: number; elapsed?: number }
28
+
29
+ function agentName(session: Session) {
30
+ const hit = session.title.match(/\(@([^\s()]+)\s+subagent\)\s*$/)
31
+ const name = hit?.[1] ?? session.agent ?? "agent"
32
+ return name.length > 10 ? `${name.slice(0, 9)}~` : name
33
+ }
34
+
35
+ function agentDesc(session: Session) {
36
+ const desc = session.title.replace(/\s*\(@[^\s()]+\s+subagent\)\s*$/, "")
37
+ return desc.length > 30 ? `${desc.slice(0, 29)}~` : desc
38
+ }
39
+
40
+ function formatTokens(value: number) {
41
+ if (value >= 1_000_000) return `${Math.round(value / 1_000_000)}m`
42
+ if (value >= 1000) return `${Math.round(value / 1000)}k`
43
+ return `${value}`
44
+ }
45
+
46
+ function formatElapsed(ms: number) {
47
+ const total = Math.max(0, Math.floor(ms / 1000))
48
+ const hours = Math.floor(total / 3600)
49
+ const minutes = Math.floor((total % 3600) / 60)
50
+ const seconds = total % 60
51
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`
52
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
53
+ }
54
+
55
+ function toolSummary(part: { tool: string; state: { input: Record<string, unknown> } }) {
56
+ const input = part.state.input ?? {}
57
+ const detail =
58
+ input.command ??
59
+ input.description ??
60
+ input.filePath ??
61
+ input.path ??
62
+ input.pattern ??
63
+ input.query ??
64
+ input.url ??
65
+ ""
66
+ const text = detail ? `${part.tool} ${String(detail)}` : part.tool
67
+ return text.length > 30 ? `${text.slice(0, 29)}~` : text
68
+ }
69
+
70
+ function createTracker(api: TuiPluginApi): Tracker {
71
+ return createRoot((dispose) => {
72
+ const [sessions, setSessions] = createSignal<Record<string, Session>>({})
73
+ const [runs, setRuns] = createSignal<Record<string, Run>>({})
74
+ const [history, setHistory] = createSignal<Record<string, History>>({})
75
+ const hydrated = new Set<string>()
76
+ const [now, setNow] = createSignal(Date.now())
77
+ const [collapsed, setCollapsed] = createSignal(!!api.kv.get(KV_COLLAPSED, true))
78
+ const [showArchived, setShowArchived] = createSignal(!!api.kv.get(KV_ARCHIVED, false))
79
+ const [expandedMap, setExpandedMap] = createSignal<Record<string, boolean>>({})
80
+
81
+ function track(session: Session) {
82
+ if (!session.parentID) return
83
+ setSessions((current) => {
84
+ const prev = current[session.id]
85
+ if (prev && prev.title === session.title && prev.time.updated === session.time.updated) return current
86
+ return { ...current, [session.id]: session }
87
+ })
88
+ }
89
+
90
+ function isDone(session: Session) {
91
+ const status = api.state.session.status(session.id)
92
+ if (status) return status.type === "idle"
93
+ return now() - session.time.created >= 30_000
94
+ }
95
+
96
+ async function hydrate(session: Session) {
97
+ if (hydrated.has(session.id)) return
98
+ hydrated.add(session.id)
99
+ try {
100
+ const res = await api.client.session.messages({ sessionID: session.id, limit: 10 })
101
+ const list = (res.data ?? []) as Array<{ info: AssistantMessage }>
102
+ const assistants = list.map((item) => item.info).filter((item) => item.role === "assistant")
103
+ const withTokens = assistants.findLast((item) => item.tokens && item.tokens.output > 0)
104
+ const ctx = withTokens
105
+ ? withTokens.tokens.input +
106
+ withTokens.tokens.output +
107
+ withTokens.tokens.reasoning +
108
+ withTokens.tokens.cache.read +
109
+ withTokens.tokens.cache.write
110
+ : undefined
111
+ const last = assistants[assistants.length - 1]
112
+ const end = last?.time.completed ?? last?.time.created ?? session.time.updated
113
+ const elapsed = end > session.time.created ? end - session.time.created : undefined
114
+ setHistory((current) => ({ ...current, [session.id]: { ctx, elapsed } }))
115
+ } catch {}
116
+ }
117
+
118
+ api.event.on("session.created", (event) => track(event.properties.info))
119
+ api.event.on("session.updated", (event) => track(event.properties.info))
120
+ api.event.on("session.deleted", (event) =>
121
+ setSessions((current) => {
122
+ if (!(event.properties.info.id in current)) return current
123
+ const next = { ...current }
124
+ delete next[event.properties.info.id]
125
+ return next
126
+ }),
127
+ )
128
+
129
+ void api.client.session
130
+ .list({})
131
+ .then((res) => {
132
+ for (const session of res.data ?? []) track(session)
133
+ })
134
+ .catch(() => {})
135
+
136
+ const timer = setInterval(() => setNow(Date.now()), 1000)
137
+
138
+ createEffect(() => {
139
+ for (const child of Object.values(sessions())) {
140
+ const status = api.state.session.status(child.id)
141
+ const busy = status?.type === "busy" || status?.type === "retry"
142
+ setRuns((current) => {
143
+ const run = current[child.id]
144
+ if (busy && (!run || run.end !== undefined)) return { ...current, [child.id]: { start: Date.now() } }
145
+ if (status?.type === "idle" && run && run.end === undefined)
146
+ return { ...current, [child.id]: { ...run, end: Date.now() } }
147
+ return current
148
+ })
149
+ const settled = status?.type === "idle" || (!status && Date.now() - child.time.created >= 30_000)
150
+ if (settled && api.state.session.messages(child.id).length === 0) void hydrate(child)
151
+ }
152
+ })
153
+
154
+ api.lifecycle.onDispose(dispose)
155
+
156
+ return {
157
+ active(parentID) {
158
+ return Object.values(sessions())
159
+ .filter((session) => session.parentID === parentID && !isDone(session))
160
+ .sort((a, b) => a.time.created - b.time.created)
161
+ },
162
+ archived(parentID) {
163
+ return Object.values(sessions())
164
+ .filter((session) => session.parentID === parentID && isDone(session))
165
+ .sort((a, b) => (runs()[b.id]?.end ?? b.time.updated) - (runs()[a.id]?.end ?? a.time.updated))
166
+ },
167
+ run(sessionID) {
168
+ return runs()[sessionID]
169
+ },
170
+ history(sessionID) {
171
+ return history()[sessionID]
172
+ },
173
+ now,
174
+ collapsed,
175
+ toggleCollapsed() {
176
+ const next = !collapsed()
177
+ setCollapsed(next)
178
+ api.kv.set(KV_COLLAPSED, next)
179
+ },
180
+ showArchived,
181
+ toggleArchived() {
182
+ const next = !showArchived()
183
+ setShowArchived(next)
184
+ api.kv.set(KV_ARCHIVED, next)
185
+ },
186
+ expanded(sessionID, fallback) {
187
+ return expandedMap()[sessionID] ?? fallback
188
+ },
189
+ toggleExpanded(sessionID, fallback) {
190
+ const next = !(expandedMap()[sessionID] ?? fallback)
191
+ setExpandedMap((current) => ({ ...current, [sessionID]: next }))
192
+ },
193
+ }
194
+ })
195
+ }
196
+
197
+ function Agent(props: { api: TuiPluginApi; tracker: Tracker; session: Session }) {
198
+ const api = props.api
199
+ const theme = () => api.theme.current
200
+
201
+ const status = createMemo(() => api.state.session.status(props.session.id))
202
+ const messages = createMemo(() => api.state.session.messages(props.session.id))
203
+ const lastAssistant = createMemo(() =>
204
+ messages().findLast((item): item is AssistantMessage => item.role === "assistant"),
205
+ )
206
+ const parts = createMemo(() => {
207
+ const last = messages()[messages().length - 1]
208
+ return last ? api.state.part(last.id) : []
209
+ })
210
+ const running = createMemo(() =>
211
+ parts().some(
212
+ (item) => item.type === "tool" && (item.state.status === "running" || item.state.status === "pending"),
213
+ ),
214
+ )
215
+ const tool = createMemo(() => {
216
+ const part = parts().findLast((item) => item.type === "tool")
217
+ return part as { tool: string; state: { status: string; input: Record<string, unknown> } } | undefined
218
+ })
219
+ const state = createMemo(() => {
220
+ const current = status()
221
+ if (current?.type === "retry") return { label: "retry", color: theme().error }
222
+ if (current?.type === "busy") {
223
+ if (running()) return { label: "tool", color: theme().warning }
224
+ return { label: "thinking", color: theme().accent }
225
+ }
226
+ if (lastAssistant()?.finish) return { label: "done", color: theme().textMuted }
227
+ return { label: "idle", color: theme().textMuted }
228
+ })
229
+ const model = createMemo(() => lastAssistant()?.modelID ?? props.session.model?.id ?? "?")
230
+ const ctx = createMemo(() => {
231
+ const item = messages().findLast(
232
+ (x): x is AssistantMessage => x.role === "assistant" && x.tokens.output > 0,
233
+ )
234
+ if (item) {
235
+ return (
236
+ item.tokens.input +
237
+ item.tokens.output +
238
+ item.tokens.reasoning +
239
+ item.tokens.cache.read +
240
+ item.tokens.cache.write
241
+ )
242
+ }
243
+ return props.tracker.history(props.session.id)?.ctx
244
+ })
245
+ const elapsedMs = createMemo(() => {
246
+ const run = props.tracker.run(props.session.id)
247
+ if (run) return (run.end ?? props.tracker.now()) - run.start
248
+ return props.tracker.history(props.session.id)?.elapsed
249
+ })
250
+ const open = createMemo(() => props.tracker.expanded(props.session.id, false))
251
+ const idle = () => state().label === "done" || state().label === "idle"
252
+ const summary = createMemo(() =>
253
+ [
254
+ state().label,
255
+ elapsedMs() !== undefined ? formatElapsed(elapsedMs()!) : undefined,
256
+ ]
257
+ .filter(Boolean)
258
+ .join(" "),
259
+ )
260
+
261
+ return (
262
+ <box marginBottom={1}>
263
+ <box flexDirection="row" justifyContent="space-between" width="100%">
264
+ <box
265
+ flexDirection="row"
266
+ gap={1}
267
+ onMouseDown={() => props.tracker.toggleExpanded(props.session.id, false)}
268
+ >
269
+ <text fg={state().color}>{state().label.startsWith("retry") ? "!" : idle() ? "-" : "*"}</text>
270
+ <text fg={theme().text}>
271
+ <b>[{agentName(props.session)}]</b>
272
+ </text>
273
+ <Show when={!open()}>
274
+ <text fg={theme().textMuted}>{summary()}</text>
275
+ </Show>
276
+ </box>
277
+ <box onMouseDown={() => api.route.navigate("session", { sessionID: props.session.id })}>
278
+ <text fg={theme().primary}>[view]</text>
279
+ </box>
280
+ </box>
281
+ <Show when={open()}>
282
+ <box flexDirection="row" gap={1}>
283
+ <text fg={theme().textMuted}>state:</text>
284
+ <text fg={state().color}>{state().label}</text>
285
+ </box>
286
+ <text fg={theme().textMuted}>model: {model()}</text>
287
+ <text fg={theme().textMuted}>ctx: {ctx() !== undefined ? formatTokens(ctx()!) : "-"}</text>
288
+ <text fg={theme().textMuted}>tool: {tool() ? toolSummary(tool()!) : "-"}</text>
289
+ <text fg={theme().textMuted}>
290
+ elapsed: {elapsedMs() !== undefined ? formatElapsed(elapsedMs()!) : "-"}
291
+ </text>
292
+ <text fg={theme().textMuted}>desc: {agentDesc(props.session)}</text>
293
+ <text fg={theme().textMuted}>cost: {money.format(props.session.cost ?? 0)}</text>
294
+ </Show>
295
+ </box>
296
+ )
297
+ }
298
+
299
+ function View(props: { api: TuiPluginApi; tracker: Tracker; session_id: string }) {
300
+ const theme = () => props.api.theme.current
301
+ const active = createMemo(() => props.tracker.active(props.session_id))
302
+ const archived = createMemo(() => props.tracker.archived(props.session_id))
303
+
304
+ return (
305
+ <Show when={active().length + archived().length > 0}>
306
+ <box marginBottom={1}>
307
+ <box flexDirection="row" gap={1} onMouseDown={() => props.tracker.toggleCollapsed()}>
308
+ <text fg={theme().text}>{props.tracker.collapsed() ? "▶" : "▼"}</text>
309
+ <text fg={theme().text}>
310
+ <b>Agents</b>
311
+ </text>
312
+ <text fg={theme().textMuted}>
313
+ {active().length} active {archived().length} done
314
+ </text>
315
+ </box>
316
+ <Show when={!props.tracker.collapsed()}>
317
+ <For each={active()}>
318
+ {(child) => <Agent api={props.api} tracker={props.tracker} session={child} />}
319
+ </For>
320
+ <Show when={archived().length > 0}>
321
+ <box flexDirection="row" gap={1} onMouseDown={() => props.tracker.toggleArchived()}>
322
+ <text fg={theme().textMuted}>{props.tracker.showArchived() ? "▼" : "▶"}</text>
323
+ <text fg={theme().textMuted}>Archived ({archived().length})</text>
324
+ </box>
325
+ <Show when={props.tracker.showArchived()}>
326
+ <For each={archived()}>
327
+ {(child) => <Agent api={props.api} tracker={props.tracker} session={child} />}
328
+ </For>
329
+ </Show>
330
+ </Show>
331
+ </Show>
332
+ </box>
333
+ </Show>
334
+ )
335
+ }
336
+
337
+ const tui: TuiPlugin = async (api) => {
338
+ const tracker = createTracker(api)
339
+ api.slots.register({
340
+ order: 150,
341
+ slots: {
342
+ sidebar_content(_ctx, props) {
343
+ return <View api={api} tracker={tracker} session_id={props.session_id} />
344
+ },
345
+ },
346
+ })
347
+ }
348
+
349
+ const plugin: TuiPluginModule & { id: string } = {
350
+ id: "agents-sidebar",
351
+ tui,
352
+ }
353
+
354
+ export default plugin