@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/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>
|
package/public/style.css
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
--accent: #60a5fa;
|
|
12
12
|
--good: #22c55e;
|
|
13
13
|
--bad: #ef4444;
|
|
14
|
+
--warn: #f59e0b;
|
|
14
15
|
--code-bg: #0f172a;
|
|
15
16
|
--border: #1e293b;
|
|
16
17
|
--link: #38bdf8;
|
|
@@ -89,6 +90,47 @@ pre {
|
|
|
89
90
|
margin-bottom: 8px;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/* ---------- Connection status indicator ---------- */
|
|
94
|
+
.connection-status {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
margin: 8px 0 0 !important;
|
|
99
|
+
padding: 4px 10px;
|
|
100
|
+
border-radius: 999px;
|
|
101
|
+
background: rgba(148, 163, 184, 0.1);
|
|
102
|
+
border: 1px solid var(--border);
|
|
103
|
+
font-size: 12px !important;
|
|
104
|
+
color: var(--muted) !important;
|
|
105
|
+
}
|
|
106
|
+
.conn-dot {
|
|
107
|
+
width: 9px;
|
|
108
|
+
height: 9px;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
display: inline-block;
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.04);
|
|
113
|
+
transition: background 0.2s, box-shadow 0.2s;
|
|
114
|
+
}
|
|
115
|
+
.conn-connected {
|
|
116
|
+
background: var(--good);
|
|
117
|
+
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.25), 0 0 8px rgba(34, 197, 94, 0.4);
|
|
118
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
119
|
+
}
|
|
120
|
+
.conn-fallback {
|
|
121
|
+
background: var(--warn);
|
|
122
|
+
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25);
|
|
123
|
+
}
|
|
124
|
+
.conn-disconnected {
|
|
125
|
+
background: var(--bad);
|
|
126
|
+
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.25);
|
|
127
|
+
}
|
|
128
|
+
@keyframes pulse {
|
|
129
|
+
0%, 100% { opacity: 1; }
|
|
130
|
+
50% { opacity: 0.55; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ---------- Status badges ---------- */
|
|
92
134
|
.badge {
|
|
93
135
|
display: inline-block;
|
|
94
136
|
padding: 2px 9px;
|
|
@@ -114,11 +156,16 @@ pre {
|
|
|
114
156
|
background: rgba(148, 163, 184, 0.25);
|
|
115
157
|
color: #cbd5e1;
|
|
116
158
|
}
|
|
159
|
+
.badge-paused {
|
|
160
|
+
background: rgba(245, 158, 11, 0.25);
|
|
161
|
+
color: #fcd34d;
|
|
162
|
+
}
|
|
117
163
|
.badge-unknown {
|
|
118
164
|
background: rgba(148, 163, 184, 0.18);
|
|
119
165
|
color: #cbd5e1;
|
|
120
166
|
}
|
|
121
167
|
|
|
168
|
+
/* ---------- Panels ---------- */
|
|
122
169
|
.panel {
|
|
123
170
|
margin: 22px 28px;
|
|
124
171
|
padding: 18px 22px;
|
|
@@ -134,6 +181,7 @@ pre {
|
|
|
134
181
|
color: #c7d2fe;
|
|
135
182
|
}
|
|
136
183
|
|
|
184
|
+
/* ---------- Tables ---------- */
|
|
137
185
|
table {
|
|
138
186
|
width: 100%;
|
|
139
187
|
border-collapse: collapse;
|
|
@@ -237,4 +285,4 @@ details summary {
|
|
|
237
285
|
padding: 14px 14px;
|
|
238
286
|
margin: 14px 10px;
|
|
239
287
|
}
|
|
240
|
-
}
|
|
288
|
+
}
|