@ai-setting/roy-plugin-task-show 0.3.0 → 0.5.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/README.md +310 -84
- package/dist/collector.d.ts +19 -3
- package/dist/collector.d.ts.map +1 -1
- package/dist/collector.js +42 -7
- 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 +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +69 -28
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +238 -94
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +104 -53
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +79 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -1
- package/dist/types.js.map +1 -1
- package/dist/url-injector.d.ts +15 -37
- package/dist/url-injector.d.ts.map +1 -1
- package/dist/url-injector.js +15 -47
- package/dist/url-injector.js.map +1 -1
- package/package.json +2 -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/dist/url-injector.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-injector.js","sourceRoot":"","sources":["../src/url-injector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"url-injector.js","sourceRoot":"","sources":["../src/url-injector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAA0C,EAC1C,MAAuB;IAEvB,qDAAqD;IACrD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;IAC7D,OAAO,UAAU,IAAI,IAAI,GAAG,CAAC,IAAI,SAAS,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,GAA0C;IAE1C,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;IAC7D,OAAO,UAAU,IAAI,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC;AACvC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-setting/roy-plugin-task-show",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "roy-agent plugin: visualize task solving process via tool call flow on a local web service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
17
|
"dev": "tsc -p tsconfig.json --watch",
|
|
18
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
18
19
|
"test": "bun test",
|
|
19
20
|
"start:demo": "bun run scripts/run-demo.ts",
|
|
20
21
|
"verify": "bun run scripts/verify-service.ts",
|
package/plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-setting/roy-plugin-task-show",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "tool-plugin",
|
|
5
|
-
"description": "Visualize the tool call chain of a task on a local web service
|
|
5
|
+
"description": "Visualize the tool call chain of a task on a local web service with real-time SSE updates. v0.5.0+: page refreshes stream over GET /api/events (Server-Sent Events). Subscribes to tool:before.execute, tool:after.execute, task:before.create, task:after.create, task:after.complete (preferred, 2026-07-10+), and task:after.update (legacy fallback).",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"point": "tool:after.execute",
|
|
14
|
-
"purpose": "Collect every tool invocation: tool name, args, result, timing, success"
|
|
14
|
+
"purpose": "Collect every tool invocation: tool name, args, result, timing, success. Broadcasts a `tool.recorded` SSE event so the frontend timeline + progress bar update live."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"point": "task:before.create",
|
|
18
|
+
"purpose": "Open a fresh TaskSession before the task runs (v0.5.0+). Mints the visualization URL and broadcasts a `task.created` SSE event so the UI can render the card immediately, even if no tool call fires."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"point": "task:after.create",
|
|
22
|
+
"purpose": "Mark the session ready for tracking (v0.5.0+). For hosts that emit only this hook (no `task:before.create`), the plugin opens the session here. Broadcasts a `task.created` SSE event when the session is newly minted."
|
|
15
23
|
},
|
|
16
24
|
{
|
|
17
25
|
"point": "task:after.complete",
|
|
18
|
-
"purpose": "PREFERRED hook point (roy-agent 2026-07-10+). When a task transitions to a terminal status (completed/failed/cancelled
|
|
26
|
+
"purpose": "PREFERRED hook point (roy-agent 2026-07-10+). When a task transitions to a terminal status (completed/failed/cancelled), finalize the session and broadcast a `task.completed` SSE event. terminalStatus field is explicit — no status filtering required. The handler dedupes against any `task:after.update` that may also fire on the same transition."
|
|
19
27
|
},
|
|
20
28
|
{
|
|
21
29
|
"point": "task:after.update",
|
|
22
|
-
"purpose": "
|
|
30
|
+
"purpose": "GENERIC status update hook (v0.5.0+). Used for non-terminal transitions (running → paused → running) AND as a legacy fallback for hosts predating the AFTER_COMPLETE hook point. Broadcasts `task.updated` for non-terminal status, `task.completed` for terminal status. The internal dedup set guarantees at most one terminal broadcast per task even if both hooks fire."
|
|
23
31
|
}
|
|
24
32
|
],
|
|
25
33
|
"commands": [
|
|
@@ -49,17 +57,31 @@
|
|
|
49
57
|
"default": 50,
|
|
50
58
|
"description": "Maximum number of completed tasks to keep in memory (older ones are evicted)"
|
|
51
59
|
},
|
|
52
|
-
"urlInjectEnabled": {
|
|
53
|
-
"type": "boolean",
|
|
54
|
-
"default": true,
|
|
55
|
-
"description": "Append a \ud83d\udcca visualization URL to the last tool result when a task completes"
|
|
56
|
-
},
|
|
57
60
|
"publicDir": {
|
|
58
61
|
"type": "string",
|
|
59
62
|
"default": "public",
|
|
60
63
|
"description": "Directory holding the static visualization frontend (HTML/JS/CSS)"
|
|
61
64
|
}
|
|
62
65
|
},
|
|
66
|
+
"endpoints": {
|
|
67
|
+
"index": "GET / — index of recent task sessions",
|
|
68
|
+
"taskDetail": "GET /task/:taskId — detail page (mermaid flow + tables)",
|
|
69
|
+
"apiSessions": "GET /api/sessions — JSON list of sessions",
|
|
70
|
+
"apiSessionDetail": "GET /api/sessions/:taskId — JSON detail of one session",
|
|
71
|
+
"apiEvents": "GET /api/events — Server-Sent Events stream (real-time push)",
|
|
72
|
+
"static": "GET /static/* — static frontend assets (CSS/JS)"
|
|
73
|
+
},
|
|
74
|
+
"sse": {
|
|
75
|
+
"events": [
|
|
76
|
+
"snapshot — initial state dump on connect",
|
|
77
|
+
"task.created — new task session opened",
|
|
78
|
+
"task.updated — non-terminal status change",
|
|
79
|
+
"task.completed — task transitioned to a terminal status",
|
|
80
|
+
"tool.recorded — tool call appended to a session"
|
|
81
|
+
],
|
|
82
|
+
"heartbeatMs": 15000,
|
|
83
|
+
"snapshotFrame": "event: snapshot\ndata: {type:'snapshot', data:{sessions:[...]}}\n\n"
|
|
84
|
+
},
|
|
63
85
|
"roy-agent": {
|
|
64
86
|
"minVersion": "1.6.0",
|
|
65
87
|
"extends": "BasePlugin",
|
|
@@ -67,4 +89,4 @@
|
|
|
67
89
|
},
|
|
68
90
|
"repository": "github:ai-setting/roy-plugin-task-visualize",
|
|
69
91
|
"bugs": "https://github.com/ai-setting/roy-plugin-task-visualize/issues"
|
|
70
|
-
}
|
|
92
|
+
}
|
package/public/app.js
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
/* ------------------------------------------------------------------------- */
|
|
2
|
-
/* roy-plugin-task-show —
|
|
2
|
+
/* roy-plugin-task-show — frontend with SSE real-time updates */
|
|
3
3
|
/* ------------------------------------------------------------------------- */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Lightweight in-browser state for the index page:
|
|
7
|
+
* - map of taskId → session
|
|
8
|
+
* - simple debounce/throttle for re-render
|
|
9
|
+
*
|
|
10
|
+
* The page renders:
|
|
11
|
+
* - a "connection" indicator (green when SSE is up, red when on fallback)
|
|
12
|
+
* - a stats summary (total sessions, running, completed, failed)
|
|
13
|
+
* - a task grid (one card per task with status badge + progress bar)
|
|
14
|
+
* - a live timeline (most recent tool calls across all tasks)
|
|
15
|
+
*
|
|
16
|
+
* Update path:
|
|
17
|
+
* 1. Open an EventSource on /api/events
|
|
18
|
+
* 2. On `snapshot`: replace local state with the server's snapshot
|
|
19
|
+
* 3. On `task.created` / `task.updated` / `task.completed`:
|
|
20
|
+
* replace or insert the session in state, then re-render
|
|
21
|
+
* 4. On `tool.recorded`:
|
|
22
|
+
* append the tool call to the session, update progress, re-render
|
|
23
|
+
* 5. If the SSE connection drops, fall back to a 3-second poll of
|
|
24
|
+
* /api/sessions (the legacy behavior)
|
|
8
25
|
*/
|
|
26
|
+
|
|
9
27
|
(function attachArgsExpander() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Allow each <tr data-args="..."> to expand a JSON dialog on click.
|
|
30
|
+
* `data-args` carries a single-line JSON string; we pretty-print it on demand.
|
|
31
|
+
*/
|
|
32
|
+
document.addEventListener("click", (e) => {
|
|
33
|
+
const target = e.target;
|
|
34
|
+
if (!(target instanceof Element)) return;
|
|
35
|
+
const row = target.closest("tr[data-args]");
|
|
36
|
+
if (!row) return;
|
|
37
|
+
const raw = row.getAttribute("data-args") || "";
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
parsed = raw;
|
|
43
|
+
}
|
|
44
|
+
openDialog({
|
|
45
|
+
title: row.querySelector("td:nth-child(2)")?.textContent?.trim() || "Tool call",
|
|
46
|
+
body: JSON.stringify(parsed, null, 2),
|
|
25
47
|
});
|
|
26
48
|
});
|
|
27
49
|
|
|
@@ -83,51 +105,381 @@
|
|
|
83
105
|
})();
|
|
84
106
|
|
|
85
107
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* manual refresh. Lightweight: single GET, no debouncing.
|
|
108
|
+
* Initialize a live timeline on the per-task page (when `data-task-id` is
|
|
109
|
+
* present). We re-render the timeline on every `tool.recorded` event.
|
|
89
110
|
*/
|
|
90
|
-
(function
|
|
91
|
-
const banner = document.querySelector(
|
|
111
|
+
(function attachTaskPageTimeline() {
|
|
112
|
+
const banner = document.querySelector("[data-live-refresh][data-task-id]");
|
|
92
113
|
if (!banner) return;
|
|
93
|
-
const taskId = banner.getAttribute(
|
|
94
|
-
|
|
114
|
+
const taskId = Number(banner.getAttribute("data-task-id"));
|
|
115
|
+
if (!Number.isFinite(taskId)) return;
|
|
116
|
+
|
|
117
|
+
// Subscribe to SSE for this task only.
|
|
118
|
+
try {
|
|
119
|
+
const es = new EventSource(`/api/events`);
|
|
120
|
+
es.addEventListener("snapshot", (ev) => {
|
|
121
|
+
const data = JSON.parse((ev).data);
|
|
122
|
+
const session = (data.data?.sessions || []).find((s) => s.taskId === taskId);
|
|
123
|
+
if (session) applyTaskUpdate(session);
|
|
124
|
+
});
|
|
125
|
+
["task.created", "task.updated", "task.completed"].forEach((type) => {
|
|
126
|
+
es.addEventListener(type, (ev) => {
|
|
127
|
+
const data = JSON.parse((ev).data);
|
|
128
|
+
const session = data.data?.session;
|
|
129
|
+
if (session && session.taskId === taskId) applyTaskUpdate(session);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
es.addEventListener("tool.recorded", (ev) => {
|
|
133
|
+
const data = JSON.parse((ev).data);
|
|
134
|
+
const session = data.data?.session;
|
|
135
|
+
if (session && session.taskId === taskId) applyTaskUpdate(session);
|
|
136
|
+
});
|
|
137
|
+
es.onerror = () => {
|
|
138
|
+
// EventSource will auto-reconnect; nothing to do here.
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore — the page will still render statically
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applyTaskUpdate(session) {
|
|
145
|
+
// Update status badge.
|
|
146
|
+
const badge = document.querySelector(".badge");
|
|
147
|
+
if (badge) {
|
|
148
|
+
const cls = `badge badge-${session.status}`;
|
|
149
|
+
badge.className = cls;
|
|
150
|
+
badge.textContent = session.status;
|
|
151
|
+
}
|
|
152
|
+
// Update meta line.
|
|
153
|
+
const meta = document.querySelector(".topbar .meta");
|
|
154
|
+
if (meta) {
|
|
155
|
+
const totalMs = session.toolCalls.reduce((a, c) => a + c.durationMs, 0);
|
|
156
|
+
const ok = session.toolCalls.filter((c) => c.success).length;
|
|
157
|
+
const fail = session.toolCalls.length - ok;
|
|
158
|
+
meta.innerHTML =
|
|
159
|
+
`<span class="badge badge-${session.status}">${session.status}</span>` +
|
|
160
|
+
` · ${session.toolCalls.length} tool call(s)` +
|
|
161
|
+
` · ${totalMs} ms total` +
|
|
162
|
+
` · ${ok} ok / ${fail} fail` +
|
|
163
|
+
` · started ${new Date(session.startedAt).toISOString().replace("T", " ").slice(0, 19)}` +
|
|
164
|
+
(session.endedAt ? ` · ended ${new Date(session.endedAt).toISOString().replace("T", " ").slice(0, 19)}` : "");
|
|
165
|
+
}
|
|
166
|
+
// Re-render tool calls table in place.
|
|
167
|
+
const tbody = document.querySelector("table.toolcalls tbody");
|
|
168
|
+
if (tbody) {
|
|
169
|
+
tbody.innerHTML = session.toolCalls
|
|
170
|
+
.map((c) => renderToolCallRow(c))
|
|
171
|
+
.join("");
|
|
172
|
+
}
|
|
173
|
+
// Re-render stats table.
|
|
174
|
+
const statsTbody = document.querySelector("table.stats tbody");
|
|
175
|
+
if (statsTbody) {
|
|
176
|
+
const aggregate = aggregateStats(session.toolCalls);
|
|
177
|
+
statsTbody.innerHTML =
|
|
178
|
+
Object.entries(aggregate)
|
|
179
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
180
|
+
.map(
|
|
181
|
+
([tool, s]) =>
|
|
182
|
+
`<tr><td><code>${escapeHtmlAttr(tool)}</code></td>` +
|
|
183
|
+
`<td>${s.count}</td><td>${s.success}</td><td>${s.fail}</td><td>${s.totalMs} ms</td></tr>`,
|
|
184
|
+
)
|
|
185
|
+
.join("") || `<tr><td colspan="5" class="empty">No tool calls recorded.</td></tr>`;
|
|
186
|
+
}
|
|
187
|
+
// Re-render mermaid diagram.
|
|
188
|
+
const mermaidDiv = document.querySelector(".mermaid");
|
|
189
|
+
if (mermaidDiv && window.mermaid) {
|
|
190
|
+
mermaidDiv.innerHTML = buildMermaidSource(session);
|
|
191
|
+
try {
|
|
192
|
+
window.mermaid.run({ nodes: [mermaidDiv] });
|
|
193
|
+
} catch {
|
|
194
|
+
// ignore — re-render is best-effort
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderToolCallRow(call) {
|
|
200
|
+
const argsJson = JSON.stringify(redactArgs(call.args || {}));
|
|
201
|
+
const preview = call.error
|
|
202
|
+
? (call.error.slice(0, 200) + (call.error.length > 200 ? "…" : ""))
|
|
203
|
+
: (call.outputPreview.slice(0, 240) + (call.outputPreview.length > 240 ? "…" : ""));
|
|
204
|
+
const status = call.success ? "ok" : "fail";
|
|
205
|
+
const attach = call.hasAttachment ? ' <span class="attach">📎</span>' : "";
|
|
206
|
+
return (
|
|
207
|
+
`<tr class="row-${status}" data-args="${escapeHtmlAttr(argsJson)}">` +
|
|
208
|
+
`<td class="seq">${call.sequence}</td>` +
|
|
209
|
+
`<td><code>${escapeHtml(call.toolName)}</code>${attach}</td>` +
|
|
210
|
+
`<td class="status status-${status}">${status}</td>` +
|
|
211
|
+
`<td>${call.durationMs}</td>` +
|
|
212
|
+
`<td>${escapeHtml(formatTs(call.timestamp))}</td>` +
|
|
213
|
+
`<td class="args">${escapeHtml(argsJson)}</td>` +
|
|
214
|
+
`<td class="result">${escapeHtml(preview)}</td>` +
|
|
215
|
+
`</tr>`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function aggregateStats(calls) {
|
|
220
|
+
const out = {};
|
|
221
|
+
for (const c of calls) {
|
|
222
|
+
const cur = out[c.toolName] ?? { count: 0, success: 0, fail: 0, totalMs: 0 };
|
|
223
|
+
cur.count += 1;
|
|
224
|
+
if (c.success) cur.success += 1;
|
|
225
|
+
else cur.fail += 1;
|
|
226
|
+
cur.totalMs += c.durationMs;
|
|
227
|
+
out[c.toolName] = cur;
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
95
231
|
|
|
96
|
-
|
|
232
|
+
function buildMermaidSource(session) {
|
|
233
|
+
const lines = [`flowchart TD`];
|
|
234
|
+
lines.push(` classDef ok fill:#dcfce7,stroke:#16a34a,color:#064e3b`);
|
|
235
|
+
lines.push(` classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d`);
|
|
236
|
+
lines.push(` classDef runtime fill:#dbeafe,stroke:#2563eb,color:#1e3a8a`);
|
|
237
|
+
lines.push(` Start([Task #${session.taskId} start]):::runtime`);
|
|
238
|
+
let prev = "Start";
|
|
239
|
+
if (session.toolCalls.length === 0) {
|
|
240
|
+
lines.push(` End([end]):::runtime`);
|
|
241
|
+
lines.push(` Start --> End`);
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
session.toolCalls.forEach((call) => {
|
|
245
|
+
const nodeId = `n${call.sequence}`;
|
|
246
|
+
const label = `${call.sequence}. ${escapeMermaid(call.toolName)}${call.hasAttachment ? " 📎" : ""}`;
|
|
247
|
+
lines.push(` ${nodeId}["${label}<br/><small>${call.durationMs}ms · ${call.success ? "ok" : "FAIL"}</small>"]`);
|
|
248
|
+
lines.push(` class ${nodeId} ${call.success ? "ok" : "fail"}`);
|
|
249
|
+
lines.push(` ${prev} --> ${nodeId}`);
|
|
250
|
+
prev = nodeId;
|
|
251
|
+
});
|
|
252
|
+
const endLabel = session.status === "failed" ? `❌ Task ${session.status}` : `✅ Task ${session.status}`;
|
|
253
|
+
lines.push(` End([${endLabel}]):::runtime`);
|
|
254
|
+
lines.push(` ${prev} --> End`);
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatTs(ms) {
|
|
97
259
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`<td><span class="badge badge-${s.status}">${s.status}</span></td>` +
|
|
113
|
-
`<td>${(s.toolCalls || []).length} calls · ${(s.toolCalls || []).reduce((a, c) => a + c.durationMs, 0)} ms</td>` +
|
|
114
|
-
`<td>${(s.title || '(no title)')}</td>` +
|
|
115
|
-
`<td>${new Date(s.startedAt).toISOString().replace('T', ' ').slice(0, 19)}</td>` +
|
|
116
|
-
`<td class="tools">${tools}</td>` +
|
|
117
|
-
'</tr>'
|
|
118
|
-
);
|
|
119
|
-
})
|
|
120
|
-
.join('');
|
|
121
|
-
} else if (data && data.taskId != null) {
|
|
122
|
-
if (data.status !== initialStatus) {
|
|
123
|
-
// Reload to pick up the new status badge & flow chart.
|
|
124
|
-
location.reload();
|
|
125
|
-
}
|
|
260
|
+
return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
261
|
+
} catch {
|
|
262
|
+
return String(ms);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function redactArgs(args) {
|
|
267
|
+
const clone = {};
|
|
268
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
269
|
+
const lk = k.toLowerCase();
|
|
270
|
+
if (/(api[_-]?key|token|password|pass|secret)/.test(lk)) {
|
|
271
|
+
clone[k] = "***REDACTED***";
|
|
272
|
+
} else {
|
|
273
|
+
clone[k] = v;
|
|
126
274
|
}
|
|
127
|
-
} catch (e) {
|
|
128
|
-
// network blip — try again next tick
|
|
129
275
|
}
|
|
276
|
+
return clone;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function escapeMermaid(s) {
|
|
280
|
+
return String(s).replace(/[<>"#]/g, "").replace(/[^A-Za-z0-9_.\-]/g, "_");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function escapeHtml(s) {
|
|
284
|
+
return String(s)
|
|
285
|
+
.replace(/&/g, "&")
|
|
286
|
+
.replace(/</g, "<")
|
|
287
|
+
.replace(/>/g, ">")
|
|
288
|
+
.replace(/"/g, """)
|
|
289
|
+
.replace(/'/g, "'");
|
|
130
290
|
}
|
|
131
291
|
|
|
132
|
-
|
|
292
|
+
function escapeHtmlAttr(s) {
|
|
293
|
+
return escapeHtml(s);
|
|
294
|
+
}
|
|
133
295
|
})();
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Index page: subscribe to SSE and keep the table + cards in sync. Falls
|
|
299
|
+
* back to a 3-second poll if EventSource is unavailable (very old browsers)
|
|
300
|
+
* or if the connection is dropped.
|
|
301
|
+
*/
|
|
302
|
+
(function attachIndexLiveRefresh() {
|
|
303
|
+
const banner = document.querySelector("[data-live-refresh]");
|
|
304
|
+
if (!banner) return;
|
|
305
|
+
// Per-task page also has data-live-refresh; skip if it has data-task-id.
|
|
306
|
+
if (banner.hasAttribute("data-task-id")) return;
|
|
307
|
+
|
|
308
|
+
const tbody = document.querySelector(".sessions tbody");
|
|
309
|
+
if (!tbody) return;
|
|
310
|
+
|
|
311
|
+
// Local state.
|
|
312
|
+
const sessions = new Map();
|
|
313
|
+
|
|
314
|
+
let usePollingFallback = typeof EventSource === "undefined";
|
|
315
|
+
let pollTimer = null;
|
|
316
|
+
let source = null;
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------
|
|
319
|
+
// Helpers
|
|
320
|
+
// ---------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
function setConnectionStatus(state, label) {
|
|
323
|
+
const el = document.querySelector("[data-connection]");
|
|
324
|
+
if (!el) return;
|
|
325
|
+
const dot = el.querySelector(".conn-dot");
|
|
326
|
+
const text = el.querySelector(".conn-label");
|
|
327
|
+
if (dot) {
|
|
328
|
+
dot.className =
|
|
329
|
+
"conn-dot " +
|
|
330
|
+
(state === "connected"
|
|
331
|
+
? "conn-connected"
|
|
332
|
+
: state === "fallback"
|
|
333
|
+
? "conn-fallback"
|
|
334
|
+
: "conn-disconnected");
|
|
335
|
+
}
|
|
336
|
+
if (text) text.textContent = label;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderIndex() {
|
|
340
|
+
const arr = Array.from(sessions.values()).sort(
|
|
341
|
+
(a, b) => b.startedAt - a.startedAt,
|
|
342
|
+
);
|
|
343
|
+
if (arr.length === 0) {
|
|
344
|
+
tbody.innerHTML =
|
|
345
|
+
`<tr><td colspan="6" class="empty">No tasks recorded yet. Trigger a tool call (e.g. send a message in <code>interactive</code> mode) and watch this page update live.</td></tr>`;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
tbody.innerHTML = arr.map(renderSessionRow).join("");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderSessionRow(s) {
|
|
352
|
+
const tools = new Set((s.toolCalls || []).map((c) => c.toolName));
|
|
353
|
+
const totalMs = (s.toolCalls || []).reduce((a, c) => a + c.durationMs, 0);
|
|
354
|
+
const status = s.status || "running";
|
|
355
|
+
return (
|
|
356
|
+
`<tr>` +
|
|
357
|
+
`<td><a href="/task/${s.taskId}">#${s.taskId}</a></td>` +
|
|
358
|
+
`<td><span class="badge badge-${status}">${escapeHtml(status)}</span></td>` +
|
|
359
|
+
`<td>${(s.toolCalls || []).length} calls · ${totalMs} ms</td>` +
|
|
360
|
+
`<td>${escapeHtml(s.title || "(no title)")}</td>` +
|
|
361
|
+
`<td>${formatTs(s.startedAt)}</td>` +
|
|
362
|
+
`<td class="tools">${Array.from(tools).map((t) => `<code>${escapeHtml(t)}</code>`).join(" ")}</td>` +
|
|
363
|
+
`</tr>`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function upsertSession(session) {
|
|
368
|
+
if (!session || typeof session.taskId !== "number") return;
|
|
369
|
+
sessions.set(session.taskId, session);
|
|
370
|
+
renderIndex();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------
|
|
374
|
+
// SSE path
|
|
375
|
+
// ---------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
function openSSE() {
|
|
378
|
+
if (typeof EventSource === "undefined") {
|
|
379
|
+
startPollingFallback();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
source = new EventSource("/api/events");
|
|
384
|
+
} catch {
|
|
385
|
+
startPollingFallback();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
source.addEventListener("open", () => {
|
|
390
|
+
setConnectionStatus("connected", "Live (SSE)");
|
|
391
|
+
// If we were polling, stop now.
|
|
392
|
+
if (pollTimer) {
|
|
393
|
+
clearInterval(pollTimer);
|
|
394
|
+
pollTimer = null;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
source.addEventListener("snapshot", (ev) => {
|
|
399
|
+
const data = JSON.parse(ev.data);
|
|
400
|
+
const list = data.data?.sessions || [];
|
|
401
|
+
sessions.clear();
|
|
402
|
+
for (const s of list) sessions.set(s.taskId, s);
|
|
403
|
+
renderIndex();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
["task.created", "task.updated", "task.completed"].forEach((type) => {
|
|
407
|
+
source.addEventListener(type, (ev) => {
|
|
408
|
+
const data = JSON.parse(ev.data);
|
|
409
|
+
const session = data.data?.session;
|
|
410
|
+
if (session) upsertSession(session);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
source.addEventListener("tool.recorded", (ev) => {
|
|
415
|
+
const data = JSON.parse(ev.data);
|
|
416
|
+
const session = data.data?.session;
|
|
417
|
+
if (session) upsertSession(session);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
source.addEventListener("error", () => {
|
|
421
|
+
// EventSource auto-reconnects, but if it stays down for too long
|
|
422
|
+
// we kick in the polling fallback.
|
|
423
|
+
setConnectionStatus("disconnected", "Reconnecting…");
|
|
424
|
+
if (!pollTimer) {
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
if (source && source.readyState === EventSource.CLOSED) {
|
|
427
|
+
startPollingFallback();
|
|
428
|
+
}
|
|
429
|
+
}, 3000);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---------------------------------------------------------------------
|
|
435
|
+
// Polling fallback (3s) — the legacy v0.1.0–v0.4.0 behavior
|
|
436
|
+
// ---------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
function startPollingFallback() {
|
|
439
|
+
usePollingFallback = true;
|
|
440
|
+
setConnectionStatus("fallback", "Polling (3s fallback)");
|
|
441
|
+
pollTimer = setInterval(pollOnce, 3000);
|
|
442
|
+
pollOnce();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function pollOnce() {
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch("/api/sessions", { headers: { accept: "application/json" } });
|
|
448
|
+
if (!res.ok) return;
|
|
449
|
+
const list = await res.json();
|
|
450
|
+
sessions.clear();
|
|
451
|
+
for (const s of list) sessions.set(s.taskId, s);
|
|
452
|
+
renderIndex();
|
|
453
|
+
} catch {
|
|
454
|
+
// network blip — try again next tick
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------
|
|
459
|
+
// Initial paint
|
|
460
|
+
// ---------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
setConnectionStatus("disconnected", "Connecting…");
|
|
463
|
+
if (!usePollingFallback) {
|
|
464
|
+
openSSE();
|
|
465
|
+
} else {
|
|
466
|
+
startPollingFallback();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function formatTs(ms) {
|
|
470
|
+
try {
|
|
471
|
+
return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
472
|
+
} catch {
|
|
473
|
+
return String(ms);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function escapeHtml(s) {
|
|
478
|
+
return String(s)
|
|
479
|
+
.replace(/&/g, "&")
|
|
480
|
+
.replace(/</g, "<")
|
|
481
|
+
.replace(/>/g, ">")
|
|
482
|
+
.replace(/"/g, """)
|
|
483
|
+
.replace(/'/g, "'");
|
|
484
|
+
}
|
|
485
|
+
})();
|
package/public/index.html
CHANGED
|
@@ -12,23 +12,41 @@
|
|
|
12
12
|
Standalone landing page served by the visualization service. The plugin
|
|
13
13
|
replaces the index when sessions exist.
|
|
14
14
|
</p>
|
|
15
|
+
<p class="meta connection-status" data-connection>
|
|
16
|
+
<span class="conn-dot conn-disconnected"></span>
|
|
17
|
+
<span class="conn-label">Connecting…</span>
|
|
18
|
+
</p>
|
|
15
19
|
</header>
|
|
16
20
|
<section class="panel">
|
|
17
21
|
<h2>No sessions yet</h2>
|
|
18
22
|
<p class="empty">
|
|
19
23
|
The <code>roy-agent</code> host process has not recorded any tool
|
|
20
24
|
calls yet. Trigger a session — for example by sending a message in
|
|
21
|
-
<code>interactive</code> mode — and
|
|
22
|
-
|
|
23
|
-
<p>
|
|
24
|
-
Useful endpoints while you wait:
|
|
25
|
-
<ul>
|
|
26
|
-
<li><a href="/api/sessions">/api/sessions</a> — JSON list of recorded sessions.</li>
|
|
27
|
-
<li><a href="/static/style.css">/static/style.css</a> — bundled CSS.</li>
|
|
28
|
-
<li><a href="/static/app.js">/static/app.js</a> — bundled JS.</li>
|
|
29
|
-
</ul>
|
|
25
|
+
<code>interactive</code> mode — and the page will update live via
|
|
26
|
+
Server-Sent Events.
|
|
30
27
|
</p>
|
|
28
|
+
<table class="sessions">
|
|
29
|
+
<thead>
|
|
30
|
+
<tr><th>Task ID</th><th>Status</th><th>Stats</th><th>Title</th><th>Started</th><th>Tools</th></tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody></tbody>
|
|
33
|
+
</table>
|
|
34
|
+
</section>
|
|
35
|
+
<section class="panel">
|
|
36
|
+
<h2>How to read this page</h2>
|
|
37
|
+
<ul>
|
|
38
|
+
<li>The page subscribes to <code>/api/events</code> (SSE) for real-time updates.</li>
|
|
39
|
+
<li>If the SSE connection fails, a 3-second polling fallback keeps the page fresh.</li>
|
|
40
|
+
<li>Useful endpoints:
|
|
41
|
+
<ul>
|
|
42
|
+
<li><a href="/api/sessions">/api/sessions</a> — JSON list of recorded sessions.</li>
|
|
43
|
+
<li><a href="/api/events">/api/events</a> — SSE event stream.</li>
|
|
44
|
+
<li><a href="/static/style.css">/static/style.css</a> — bundled CSS.</li>
|
|
45
|
+
<li><a href="/static/app.js">/static/app.js</a> — bundled JS.</li>
|
|
46
|
+
</ul>
|
|
47
|
+
</li>
|
|
48
|
+
</ul>
|
|
31
49
|
</section>
|
|
32
50
|
<script src="/static/app.js"></script>
|
|
33
51
|
</body>
|
|
34
|
-
</html>
|
|
52
|
+
</html>
|