@ai-setting/roy-plugin-task-show 0.4.0 → 0.5.1
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/README.md +299 -216
- package/dist/collector.d.ts +11 -0
- package/dist/collector.d.ts.map +1 -1
- package/dist/collector.js +15 -0
- package/dist/collector.js.map +1 -1
- package/dist/event-bus.d.ts +98 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +210 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +67 -64
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +234 -148
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +66 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +294 -99
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +78 -60
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -21
- package/dist/types.js.map +1 -1
- package/dist/url-injector.d.ts +13 -48
- package/dist/url-injector.d.ts.map +1 -1
- package/dist/url-injector.js +13 -58
- package/dist/url-injector.js.map +1 -1
- package/package.json +1 -1
- package/plugin.json +33 -11
- package/public/app.js +409 -57
- package/public/index.html +28 -10
- package/public/style.css +49 -1
package/README.md
CHANGED
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
# roy-plugin-task-show
|
|
2
2
|
|
|
3
3
|
> A [roy-agent](https://example/roy-agent) plugin that visualizes the tool-call
|
|
4
|
-
> chain of every task on a local web service
|
|
5
|
-
>
|
|
4
|
+
> chain of every task on a local web service with **real-time Server-Sent
|
|
5
|
+
> Events** for live page updates.
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
9
|
When loaded into a `roy-agent` host, this plugin:
|
|
10
10
|
|
|
11
11
|
1. Listens to the `tool:before.execute`, `tool:after.execute`,
|
|
12
|
-
`task:after.
|
|
13
|
-
hook points.
|
|
12
|
+
`task:before.create`, `task:after.create`, `task:after.complete`
|
|
13
|
+
(preferred) and `task:after.update` (legacy fallback) hook points.
|
|
14
14
|
2. Records every tool invocation (tool name, args, result preview, duration,
|
|
15
15
|
success flag, timestamp) into an in-memory collector keyed by task id.
|
|
16
|
-
3.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`
|
|
16
|
+
3. **Pushes every state change over a Server-Sent Events stream** served by
|
|
17
|
+
the plugin's local HTTP service. The frontend subscribes to
|
|
18
|
+
`GET /api/events` and re-renders the page within milliseconds of:
|
|
19
|
+
- a new task being created (`task.created`)
|
|
20
|
+
- a tool call being recorded (`tool.recorded`)
|
|
21
|
+
- a task status changing (`task.updated`)
|
|
22
|
+
- a task transitioning to a terminal status (`task.completed`)
|
|
22
23
|
4. Serves the visualization on a local HTTP server (default
|
|
23
|
-
`http://127.0.0.1:7788/`)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
>
|
|
24
|
+
`http://127.0.0.1:7788/`) — index page, per-task detail page, and the
|
|
25
|
+
`/api/events` SSE stream.
|
|
26
|
+
|
|
27
|
+
The result: every task gets a clickable link the user can open in a
|
|
28
|
+
browser. Once open, the page **stays in sync** with the running agent
|
|
29
|
+
loop — tool calls appear as they happen, status badges update in
|
|
30
|
+
real-time, and the progress bar grows with each call. No manual refresh
|
|
31
|
+
needed.
|
|
32
|
+
|
|
33
|
+
> **v0.5.0+** — the plugin no longer uses `env.notify`. The previous
|
|
34
|
+
> `env.notify({type:"visualization_ready"})` mechanism has been replaced
|
|
35
|
+
> with an in-process event bus + SSE stream. This makes the plugin work
|
|
36
|
+
> with **any** roy-agent host (no NotificationChannel required) and gives
|
|
37
|
+
> the frontend proper incremental updates instead of just a one-shot URL
|
|
38
|
+
> at the end. See [SSE Migration (v0.5.0+)](#sse-migration-v050) for the
|
|
33
39
|
> migration guide.
|
|
34
40
|
|
|
35
41
|
## Repository layout
|
|
@@ -46,17 +52,20 @@ roy-plugin-task-show/
|
|
|
46
52
|
│ └── app.js
|
|
47
53
|
├── src/
|
|
48
54
|
│ ├── index.ts ← public API (re-exports)
|
|
49
|
-
│ ├── types.ts ← shared types & default config
|
|
55
|
+
│ ├── types.ts ← shared types & default config + TaskEvent
|
|
50
56
|
│ ├── collector.ts ← in-memory tool-call collector
|
|
51
|
-
│ ├── server.ts ← tiny standalone Node http server
|
|
52
|
-
│ ├──
|
|
57
|
+
│ ├── server.ts ← tiny standalone Node http server + SSE endpoint
|
|
58
|
+
│ ├── event-bus.ts ← SSE event bus (broadcast / subscribe / keep-alive)
|
|
59
|
+
│ ├── url-injector.ts ← URL builders (no more mutation helpers)
|
|
53
60
|
│ ├── plugin.ts ← the TaskShowPlugin class (BasePlugin-compatible)
|
|
54
61
|
│ └── core-stub.d.ts ← type stub for the optional core peer dep
|
|
55
62
|
├── test/
|
|
56
63
|
│ ├── collector.test.ts
|
|
57
64
|
│ ├── url-injector.test.ts
|
|
58
|
-
│ ├── server.test.ts
|
|
59
|
-
│
|
|
65
|
+
│ ├── server.test.ts ← includes SSE endpoint test
|
|
66
|
+
│ ├── plugin.test.ts ← covers all six hooks + broadcast assertions
|
|
67
|
+
│ ├── plugin-sse.test.ts ← dedicated SSE event-bus consumer tests
|
|
68
|
+
│ └── integration.test.ts ← full lifecycle through HTTP + SSE
|
|
60
69
|
└── scripts/
|
|
61
70
|
├── verify-service.ts ← end-to-end check (port 7788 + curl pages)
|
|
62
71
|
└── run-demo.ts ← demo that simulates a real task lifecycle
|
|
@@ -134,85 +143,187 @@ await taskShow.dispose();
|
|
|
134
143
|
|
|
135
144
|
The plugin registers hooks for:
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
| Hook point | Priority | Purpose |
|
|
147
|
+
| ---------------------- | -------- | ------------------------------------------------------------------------ |
|
|
148
|
+
| `tool:before.execute` | 40 | Records per-call start timestamp for accurate `durationMs` |
|
|
149
|
+
| `tool:after.execute` | 50 | Records the call into the collector and broadcasts `tool.recorded` |
|
|
150
|
+
| `task:before.create` | 55 | Mints a fresh `TaskSession` and broadcasts `task.created` |
|
|
151
|
+
| `task:after.create` | 55 | Same as above (fallback for hosts that only emit `task:after.create`) |
|
|
152
|
+
| `task:after.complete` | 60 | **PREFERRED** — broadcasts `task.completed` on terminal transition |
|
|
153
|
+
| `task:after.update` | 60 | Legacy fallback; broadcasts `task.updated` or `task.completed` |
|
|
154
|
+
|
|
155
|
+
When both `task:after.complete` AND `task:after.update` fire (newer
|
|
156
|
+
hosts do this on completion), the handler dedupes — at most one
|
|
157
|
+
`task.completed` event per task.
|
|
145
158
|
|
|
146
159
|
## Configuration
|
|
147
160
|
|
|
148
161
|
All config lives in `plugin.json` (and is overridable at construction time):
|
|
149
162
|
|
|
150
|
-
| key
|
|
151
|
-
|
|
|
152
|
-
| `port`
|
|
153
|
-
| `host`
|
|
154
|
-
| `autoStart`
|
|
155
|
-
| `maxStoredTasks`
|
|
156
|
-
| `
|
|
157
|
-
|
|
163
|
+
| key | type | default | description |
|
|
164
|
+
| ---------------- | -------- | ----------- | -------------------------------------------------------- |
|
|
165
|
+
| `port` | number | `7788` | HTTP port for the visualization service. |
|
|
166
|
+
| `host` | string | `127.0.0.1` | HTTP host. Use `0.0.0.0` to allow LAN access. |
|
|
167
|
+
| `autoStart` | boolean | `true` | Start the HTTP server when `init()` is called. |
|
|
168
|
+
| `maxStoredTasks` | number | `50` | Max sessions kept in memory. Older ones are evicted. |
|
|
169
|
+
| `publicDir` | string | `public` | Directory holding the static frontend. |
|
|
170
|
+
|
|
171
|
+
> **v0.5.0**: the previous `urlInjectEnabled` config was removed. The
|
|
172
|
+
> plugin no longer mutates tool result output.
|
|
158
173
|
|
|
159
174
|
Override at construction time:
|
|
160
175
|
|
|
161
176
|
```ts
|
|
162
177
|
createTaskShowPlugin({
|
|
163
178
|
port: 9000,
|
|
164
|
-
|
|
179
|
+
maxStoredTasks: 100,
|
|
165
180
|
});
|
|
166
181
|
```
|
|
167
182
|
|
|
168
183
|
## HTTP API
|
|
169
184
|
|
|
170
|
-
| route | description
|
|
171
|
-
| --------------------------- |
|
|
172
|
-
| `GET /` | Index of recent task sessions.
|
|
173
|
-
| `GET /task/:taskId` | Per-task page with mermaid flow + tool-call table.
|
|
174
|
-
| `GET /api/sessions` | JSON list of all sessions (newest first).
|
|
175
|
-
| `GET /api/sessions/:taskId` | JSON detail of one session (full payload).
|
|
176
|
-
|
|
|
185
|
+
| route | description |
|
|
186
|
+
| --------------------------- | ------------------------------------------------------------ |
|
|
187
|
+
| `GET /` | Index of recent task sessions. |
|
|
188
|
+
| `GET /task/:taskId` | Per-task page with mermaid flow + tool-call table. |
|
|
189
|
+
| `GET /api/sessions` | JSON list of all sessions (newest first). |
|
|
190
|
+
| `GET /api/sessions/:taskId` | JSON detail of one session (full payload). |
|
|
191
|
+
| **`GET /api/events`** | **SSE stream** — see [Server-Sent Events](#server-sent-events). |
|
|
192
|
+
| `GET /static/*` | Static frontend assets (`style.css`, `app.js`). |
|
|
177
193
|
|
|
178
194
|
If the configured `port` is already in use, the service falls back to an OS
|
|
179
195
|
assigned port and logs the actual URL to stderr.
|
|
180
196
|
|
|
197
|
+
## Server-Sent Events
|
|
198
|
+
|
|
199
|
+
The local HTTP service exposes `GET /api/events` as a standard Server-Sent
|
|
200
|
+
Events stream. Every connected client receives:
|
|
201
|
+
|
|
202
|
+
1. An initial `snapshot` frame carrying the full list of currently-known
|
|
203
|
+
sessions — useful for instant render on connect / reconnect.
|
|
204
|
+
2. A `task.created` frame whenever a new task is opened
|
|
205
|
+
(`task:before.create` or `task:after.create`).
|
|
206
|
+
3. A `tool.recorded` frame after each tool call
|
|
207
|
+
(`tool:after.execute`).
|
|
208
|
+
4. A `task.updated` frame on any non-terminal status transition
|
|
209
|
+
(`task:after.update` with `running` / `paused` / etc.).
|
|
210
|
+
5. A `task.completed` frame on terminal transition
|
|
211
|
+
(`task:after.complete` or `task:after.update` with terminal status).
|
|
212
|
+
|
|
213
|
+
A 15-second keep-alive comment (`:keep-alive`) prevents reverse proxies /
|
|
214
|
+
load balancers from dropping the connection.
|
|
215
|
+
|
|
216
|
+
### Event shape
|
|
217
|
+
|
|
218
|
+
Every event has the same envelope:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
interface TaskEvent<T> {
|
|
222
|
+
type: "task.created" | "task.updated" | "task.completed" | "tool.recorded";
|
|
223
|
+
taskId: number;
|
|
224
|
+
timestamp: number; // Unix ms
|
|
225
|
+
pluginVersion: string; // "0.5.0"
|
|
226
|
+
data: T;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The `data` payload differs per `type`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// task.created
|
|
234
|
+
data: { session: TaskSession }
|
|
235
|
+
|
|
236
|
+
// task.updated
|
|
237
|
+
data: { session: TaskSession, newStatus: TaskStatus, previousStatus?: TaskStatus }
|
|
238
|
+
|
|
239
|
+
// task.completed
|
|
240
|
+
data: { session: TaskSession, terminalStatus: TaskStatus }
|
|
241
|
+
|
|
242
|
+
// tool.recorded
|
|
243
|
+
data: { session: TaskSession, toolCall: ToolCallRecord }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Subscribing from the browser
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const source = new EventSource("/api/events");
|
|
250
|
+
source.addEventListener("snapshot", (ev) => {
|
|
251
|
+
const data = JSON.parse(ev.data);
|
|
252
|
+
console.log("initial sessions:", data.data.sessions);
|
|
253
|
+
});
|
|
254
|
+
source.addEventListener("task.created", (ev) => {
|
|
255
|
+
const data = JSON.parse(ev.data);
|
|
256
|
+
console.log("new task:", data.data.session.taskId);
|
|
257
|
+
});
|
|
258
|
+
source.addEventListener("tool.recorded", (ev) => {
|
|
259
|
+
const data = JSON.parse(ev.data);
|
|
260
|
+
console.log("tool call:", data.data.toolCall.toolName);
|
|
261
|
+
});
|
|
262
|
+
source.addEventListener("task.completed", (ev) => {
|
|
263
|
+
const data = JSON.parse(ev.data);
|
|
264
|
+
console.log("task done:", data.data.terminalStatus);
|
|
265
|
+
});
|
|
266
|
+
// EventSource auto-reconnects on connection drop. If SSE is unavailable
|
|
267
|
+
// (e.g. ancient browser), the bundled frontend falls back to a 3-second
|
|
268
|
+
// poll of /api/sessions.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Subscribing from Node / custom adapter
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import http from "node:http";
|
|
275
|
+
|
|
276
|
+
http.get("http://127.0.0.1:7788/api/events", (res) => {
|
|
277
|
+
let buf = "";
|
|
278
|
+
res.on("data", (chunk) => {
|
|
279
|
+
buf += chunk.toString("utf-8");
|
|
280
|
+
let idx;
|
|
281
|
+
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
282
|
+
const frame = buf.slice(0, idx);
|
|
283
|
+
buf = buf.slice(idx + 2);
|
|
284
|
+
// frame is `event: <type>\ndata: <json>`
|
|
285
|
+
// ...
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
181
291
|
## Architecture
|
|
182
292
|
|
|
183
|
-
The plugin runs
|
|
184
|
-
|
|
293
|
+
The plugin runs entirely inside its local HTTP server. The hook handlers
|
|
294
|
+
emit events into an in-process `EventBus` (`src/event-bus.ts`), and every
|
|
295
|
+
connected SSE client receives a frame. No host-side NotificationChannel
|
|
296
|
+
plumbing is required.
|
|
185
297
|
|
|
186
298
|
```
|
|
187
|
-
TaskShowPlugin (task
|
|
299
|
+
TaskShowPlugin (tool/task hooks)
|
|
188
300
|
│
|
|
189
|
-
│
|
|
301
|
+
│ eventBus.broadcast({type, taskId, timestamp, data})
|
|
190
302
|
▼
|
|
191
|
-
|
|
303
|
+
EventBus (in-process subscriber Set + keep-alive loop)
|
|
192
304
|
│
|
|
305
|
+
│ write frame to every connected http.ServerResponse
|
|
193
306
|
▼
|
|
194
|
-
|
|
307
|
+
TaskShowServer.handleSSE
|
|
195
308
|
│
|
|
196
|
-
│
|
|
197
|
-
│ type: "plugin.notification.visualization_ready",
|
|
198
|
-
│ metadata: { source: "plugin:roy-plugin-task-show", ...pluginMetadata },
|
|
199
|
-
│ payload: { title, content },
|
|
200
|
-
│ })
|
|
309
|
+
│ Content-Type: text/event-stream
|
|
201
310
|
▼
|
|
202
|
-
|
|
311
|
+
Browser EventSource ◄──── 3-second polling fallback if SSE unavailable
|
|
203
312
|
│
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
└──► custom sinks → log, metrics, etc.
|
|
313
|
+
▼
|
|
314
|
+
DOM updates (badge / progress / timeline / mermaid re-render)
|
|
207
315
|
```
|
|
208
316
|
|
|
209
|
-
The
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
317
|
+
The in-process bus also exposes `addListener(fn)` so custom callers
|
|
318
|
+
(CLI tools, IM adapters, log shippers) can subscribe to the same event
|
|
319
|
+
stream without going through HTTP:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const plugin = createTaskShowPlugin();
|
|
323
|
+
plugin.getEventBus().addListener((event) => {
|
|
324
|
+
console.log("event:", event.type, event.taskId);
|
|
325
|
+
});
|
|
326
|
+
```
|
|
216
327
|
|
|
217
328
|
### Plugin ↔ host version correspondence
|
|
218
329
|
|
|
@@ -221,143 +332,98 @@ that match its audience.
|
|
|
221
332
|
| `0.1.0` | pre-2026-07-10 (`task:after.update`) | Mutates tool result. **Removed** in v0.4.0 plugin. |
|
|
222
333
|
| `0.2.0` | pre-2026-07-10 (`task:after.update`) | Same as 0.1.0; tiny refactors. |
|
|
223
334
|
| `0.3.0` | `>= 2026-07-10` | Adds `tool:before.execute` for self-managed timing. |
|
|
224
|
-
| `0.4.0`
|
|
225
|
-
|
|
226
|
-
The host's `PluginEnv.notify` method is **optional** for the plugin — it
|
|
227
|
-
degrades gracefully on older hosts (logs a one-shot warn, continues
|
|
228
|
-
serving the visualization page). The plugin therefore remains
|
|
229
|
-
installable on any host version, but newer features only fire on
|
|
230
|
-
hosts implementing the v0.4.0+ contract.
|
|
335
|
+
| `0.4.0` | `>= 2026-07-10` (NotificationChannel) | Switches to `env.notify` consumer; no longer mutates output. |
|
|
336
|
+
| **`0.5.0` (this)** | **`>= 2026-07-10`** | **Adds `task:before.create` / `task:after.create`; switches from `env.notify` to in-process EventBus + SSE.** |
|
|
231
337
|
|
|
232
338
|
### Host design reference
|
|
233
339
|
|
|
234
340
|
The full host-side design — including the `NotificationChannel`
|
|
235
|
-
interface
|
|
236
|
-
`LarkCliNotificationChannel` (legacy, `@deprecated`) — is documented
|
|
341
|
+
interface and `EventBusNotificationChannel` (default) — is documented
|
|
237
342
|
in the host repository:
|
|
238
343
|
|
|
239
|
-
>
|
|
240
|
-
> version: feature/plugin-emit-notification-v2
|
|
241
|
-
> (commits `f96508f4` → `9ec78817`)
|
|
344
|
+
>
|
|
242
345
|
|
|
243
|
-
|
|
244
|
-
the authoritative reference for the `NotificationOptions` contract
|
|
245
|
-
and the metadata merge semantics.
|
|
346
|
+
## SSE Migration (v0.5.0+)
|
|
246
347
|
|
|
247
|
-
|
|
348
|
+
> **TL;DR**: stop depending on `env.notify`. The plugin now pushes events
|
|
349
|
+
> over SSE. Connect with `new EventSource('/api/events')` (browser) or
|
|
350
|
+
> `http.get(...)` (Node).
|
|
248
351
|
|
|
249
|
-
|
|
250
|
-
`env.notify(...)` with a `visualization_ready` payload. The host's
|
|
251
|
-
`NotificationChannel` (default: `EventBusNotificationChannel`) re-emits
|
|
252
|
-
this on the env event bus as `plugin.notification.visualization_ready`.
|
|
253
|
-
|
|
254
|
-
The payload looks like:
|
|
352
|
+
### Before (v0.4.0)
|
|
255
353
|
|
|
256
354
|
```ts
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"Task: solve the bug",
|
|
262
|
-
"Status: completed",
|
|
263
|
-
"Visualization: http://127.0.0.1:7788/task/1234",
|
|
264
|
-
].join("\n"),
|
|
265
|
-
metadata: {
|
|
266
|
-
taskId: 1234,
|
|
267
|
-
terminalStatus: "completed",
|
|
268
|
-
url: "http://127.0.0.1:7788/task/1234",
|
|
269
|
-
taskTitle: "solve the bug", // when present in the hook payload
|
|
270
|
-
sessionId: "sess-xyz", // when present in the hook payload
|
|
271
|
-
pluginName: "roy-plugin-task-show",
|
|
272
|
-
pluginVersion: "0.4.0", // sync with host v2 (Task #N/A)
|
|
273
|
-
notificationTimestamp: 1718000000000, // captured at emit time (Date.now())
|
|
274
|
-
},
|
|
275
|
-
}
|
|
355
|
+
// Plugin called env.notify({type:"visualization_ready", ...}).
|
|
356
|
+
// Host's EventBusNotificationChannel re-emitted on env event bus:
|
|
357
|
+
// env.pushEnvEvent({type:"plugin.notification.visualization_ready", ...})
|
|
358
|
+
// Subscribers (CLI / IM) listened on the env event bus.
|
|
276
359
|
```
|
|
277
360
|
|
|
278
|
-
|
|
279
|
-
event with the plugin that emitted it (useful when multiple versions
|
|
280
|
-
of the same plugin are installed in different workspaces). The
|
|
281
|
-
`notificationTimestamp` is captured at emit time and is distinct from
|
|
282
|
-
the host's `event.timestamp` (which the host stamps at
|
|
283
|
-
`pushEnvEvent` call time) — useful for measuring queue / delivery
|
|
284
|
-
latency downstream.
|
|
285
|
-
|
|
286
|
-
### Subscribing to the notification
|
|
287
|
-
|
|
288
|
-
The cleanest way to surface the URL to users is to subscribe to the
|
|
289
|
-
env event bus for `plugin.notification.visualization_ready`. Example
|
|
290
|
-
(in the host, e.g. `packages/cli`):
|
|
361
|
+
### After (v0.5.0+)
|
|
291
362
|
|
|
292
363
|
```ts
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
},
|
|
302
|
-
);
|
|
364
|
+
// Plugin emits via in-process EventBus; HTTP server streams frames
|
|
365
|
+
// over /api/events. Frontend connects with EventSource — no host
|
|
366
|
+
// NotificationChannel required.
|
|
367
|
+
const source = new EventSource("/api/events");
|
|
368
|
+
source.addEventListener("task.completed", (ev) => {
|
|
369
|
+
const { taskId, data } = JSON.parse(ev.data);
|
|
370
|
+
console.log(`Task ${taskId} → ${data.terminalStatus}`);
|
|
371
|
+
});
|
|
303
372
|
```
|
|
304
373
|
|
|
305
|
-
|
|
306
|
-
`TaskShowPlugin.getVisualizationUrl(taskId)` for callers that need it
|
|
307
|
-
synchronously.
|
|
374
|
+
### Why we changed
|
|
308
375
|
|
|
309
|
-
|
|
376
|
+
1. **No host dependency**: the SSE stream is served by the plugin's own
|
|
377
|
+
HTTP server. Works with any roy-agent host — no NotificationChannel
|
|
378
|
+
required.
|
|
379
|
+
2. **Incremental updates**: instead of one-shot "URL injection" on
|
|
380
|
+
terminal status, the frontend receives a frame for *every*
|
|
381
|
+
meaningful change (created / recorded / updated / completed). The
|
|
382
|
+
page updates as the agent works, not after.
|
|
383
|
+
3. **Progress bar + timeline**: with continuous updates, the frontend
|
|
384
|
+
can show a live progress bar (tool call count) and a real-time
|
|
385
|
+
timeline without manual refresh.
|
|
386
|
+
4. **Simpler host contract**: removed `env.notify` plumbing from the
|
|
387
|
+
plugin surface. The plugin's `PluginEnvLike` type no longer mentions
|
|
388
|
+
`notify`.
|
|
310
389
|
|
|
311
|
-
|
|
390
|
+
### Backward compatibility
|
|
312
391
|
|
|
313
|
-
|
|
392
|
+
The plugin no longer mutates tool result output (that mechanism was
|
|
393
|
+
removed in v0.4.0). Pin to `roy-plugin-task-show@0.4.x` if you depend
|
|
394
|
+
on the legacy `env.notify` consumer path.
|
|
314
395
|
|
|
315
|
-
|
|
316
|
-
// Plugin mutated ctx.result.output by appending a markdown banner.
|
|
317
|
-
await plugin.onTaskAfterComplete({
|
|
318
|
-
data: { task: { id: 1234, status: "completed", title: "demo" } },
|
|
319
|
-
});
|
|
320
|
-
// ctx.result.output now contains:
|
|
321
|
-
// "...original...\n---\n📊 可视化工具调用链路: <http://...>"
|
|
322
|
-
```
|
|
396
|
+
## Port handling
|
|
323
397
|
|
|
324
|
-
|
|
398
|
+
The plugin binds a local `http.Server` on the port you configure (default
|
|
399
|
+
`7788`). If that port is busy (`EADDRINUSE`), the server **probes
|
|
400
|
+
`port + 1`, `port + 2`, … in ascending order** until a free port is
|
|
401
|
+
found. Each skipped port emits a `WARN` log so it's easy to spot:
|
|
325
402
|
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
data: {
|
|
331
|
-
task: { id: 1234, status: "completed", title: "demo" },
|
|
332
|
-
terminalStatus: "completed",
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
// env.notify was called with { type: "visualization_ready", ... }
|
|
336
|
-
// ctx.result.output is unchanged.
|
|
403
|
+
```
|
|
404
|
+
[task-show:server] WARN Port 7788 busy — trying 7789
|
|
405
|
+
[task-show:server] WARN Port 7789 busy — trying 7790
|
|
406
|
+
[task-show:server] INFO HTTP service listening on http://127.0.0.1:7790/ (port rewritten from 7788 due to conflict)
|
|
337
407
|
```
|
|
338
408
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
is marked `@deprecated` but is still exported. Set
|
|
358
|
-
`urlInjectEnabled: true` in the plugin config to opt back into the
|
|
359
|
-
mutation behavior — useful for test harnesses and very old hosts that
|
|
360
|
-
don't have the `NotificationChannel` abstraction.
|
|
409
|
+
Guarantees:
|
|
410
|
+
|
|
411
|
+
- Only `EADDRINUSE` triggers a retry. Any other `listen()` error
|
|
412
|
+
(`EACCES`, `ENOTSUP`, …) is surfaced as a normal rejection — the
|
|
413
|
+
caller still owns the error and can decide what to do.
|
|
414
|
+
- Probing **never wraps** past `65535` and **never** falls back to
|
|
415
|
+
`listen(0)` (OS-assigned ephemeral port). If everything in
|
|
416
|
+
`[port, 65535]` is busy, `start()` rejects with a clear error so
|
|
417
|
+
you know to free a port or move the host elsewhere.
|
|
418
|
+
- `ServerInfo.portRewritten` is `true` whenever the bound port differs
|
|
419
|
+
from `cfg.port`. The same flag is exposed on the `ServerInfo` object
|
|
420
|
+
passed to whoever called `start()`.
|
|
421
|
+
- The probing helper (`listenWithProbing()` in `src/server.ts`) is
|
|
422
|
+
exported so the same logic can be reused or unit-tested in isolation.
|
|
423
|
+
|
|
424
|
+
If you prefer the OS-assigned-port semantics (e.g. for tests), set
|
|
425
|
+
`port: 0` in the config — the server will then call `listen(0)`
|
|
426
|
+
directly, no probing, and `portRewritten` stays `false`.
|
|
361
427
|
|
|
362
428
|
## Development
|
|
363
429
|
|
|
@@ -368,7 +434,7 @@ bun run typecheck
|
|
|
368
434
|
# Build (emit dist/)
|
|
369
435
|
bun run build
|
|
370
436
|
|
|
371
|
-
# Run unit tests (
|
|
437
|
+
# Run unit tests (63 tests across 7 files)
|
|
372
438
|
bun test
|
|
373
439
|
|
|
374
440
|
# End-to-end check (boots the service, hits it, asserts response)
|
|
@@ -410,54 +476,71 @@ It also recognizes (in priority order):
|
|
|
410
476
|
If no task id can be recovered, the call lands in a synthetic session so
|
|
411
477
|
the visualization still works.
|
|
412
478
|
|
|
413
|
-
### `task:
|
|
414
|
-
|
|
415
|
-
The plugin accepts the standard `TaskCompleteResultContext`:
|
|
479
|
+
### `task:before.create` (v0.5.0+)
|
|
416
480
|
|
|
417
481
|
```ts
|
|
418
482
|
{
|
|
419
|
-
task: { id: number,
|
|
420
|
-
|
|
421
|
-
changes: Partial<UpdateTaskOptions>,
|
|
422
|
-
sessionId: string,
|
|
423
|
-
tagService: TagService,
|
|
483
|
+
data: { task: { id: number, title?: string } }
|
|
484
|
+
// OR flat: { taskId: number, title?: string }
|
|
424
485
|
}
|
|
425
486
|
```
|
|
426
487
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
below.
|
|
488
|
+
If a session already exists for the task id (re-create / retry), its
|
|
489
|
+
`toolCalls` are preserved.
|
|
430
490
|
|
|
431
|
-
### `task:after.
|
|
491
|
+
### `task:after.create` (v0.5.0+)
|
|
492
|
+
|
|
493
|
+
Same payload as `task:before.create`. Useful for hosts that don't emit
|
|
494
|
+
`task:before.create`; the plugin opens the session here and broadcasts
|
|
495
|
+
`task.created`.
|
|
432
496
|
|
|
433
|
-
|
|
434
|
-
|
|
497
|
+
### `task:after.complete` (preferred, consumed)
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
{
|
|
501
|
+
data: {
|
|
502
|
+
task: { id: number, status: 'completed' | 'failed' | 'cancelled', title?: string },
|
|
503
|
+
terminalStatus: 'completed' | 'failed' | 'cancelled',
|
|
504
|
+
changes: Partial<UpdateTaskOptions>,
|
|
505
|
+
sessionId: string,
|
|
506
|
+
tagService: TagService,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### `task:after.update` (legacy fallback, consumed)
|
|
435
512
|
|
|
436
513
|
```ts
|
|
437
514
|
{ data: { task: { id: number, status: string, title?: string }, changes, tagService } }
|
|
438
515
|
```
|
|
439
516
|
|
|
440
|
-
The handler filters for terminal status (`completed` / `failed` /
|
|
441
|
-
`
|
|
442
|
-
that supports both hooks),
|
|
517
|
+
The handler filters for terminal status (`completed` / `failed` /
|
|
518
|
+
`cancelled`) and dedupes — if `AFTER_COMPLETE` also fires (it can, on a
|
|
519
|
+
host that supports both hooks), only one `task.completed` event is
|
|
520
|
+
broadcast.
|
|
443
521
|
|
|
444
522
|
## Limitations & follow-ups
|
|
445
523
|
|
|
446
|
-
- **Memory-only**: the collector keeps data in memory only. Restarting
|
|
447
|
-
host process drops everything. Persisting to a file would be a
|
|
448
|
-
follow-up.
|
|
449
|
-
- **Single host**: if you launch multiple `roy-agent` instances on the
|
|
450
|
-
machine, change the `port` to avoid clashes
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
- **No file persistence**: data lives only in memory. A future
|
|
459
|
-
would be a tiny SQLite sink so a restart still shows
|
|
524
|
+
- **Memory-only**: the collector keeps data in memory only. Restarting
|
|
525
|
+
the host process drops everything. Persisting to a file would be a
|
|
526
|
+
small follow-up.
|
|
527
|
+
- **Single host**: if you launch multiple `roy-agent` instances on the
|
|
528
|
+
same machine, change the `port` to avoid clashes. The server probes
|
|
529
|
+
`port + 1`, `port + 2`, ... in ascending order until a free TCP port
|
|
530
|
+
is found (sequential port probing — no random / ephemeral fallback).
|
|
531
|
+
If everything from `port` through `65535` is busy, `start()` rejects
|
|
532
|
+
with a clear error so you know to free a port or move the host
|
|
533
|
+
elsewhere.
|
|
534
|
+
- **Mermaid CDN**: the frontend loads `mermaid@10` from jsDelivr.
|
|
535
|
+
Air-gapped deployments should vendor a copy locally.
|
|
536
|
+
- **No file persistence**: data lives only in memory. A future
|
|
537
|
+
improvement would be a tiny SQLite sink so a restart still shows
|
|
538
|
+
recent tasks.
|
|
539
|
+
- **SSE-only push**: the bundled frontend uses EventSource as the
|
|
540
|
+
primary update channel. A 3-second polling fallback covers older
|
|
541
|
+
browsers and proxies that strip SSE, but it's a degraded experience.
|
|
542
|
+
WebSocket support is a possible future addition.
|
|
460
543
|
|
|
461
544
|
## License
|
|
462
545
|
|
|
463
|
-
MIT
|
|
546
|
+
MIT
|