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 +21 -0
- package/README.md +112 -0
- package/README.zh-CN.md +109 -0
- package/assets/logo-dark.svg +110 -0
- package/assets/logo-light.svg +110 -0
- package/assets/opencode-agents-monitor.gif +0 -0
- package/package.json +37 -0
- package/src/index.tsx +354 -0
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
|
package/README.zh-CN.md
ADDED
|
@@ -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>
|
|
Binary file
|
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
|