@bahulam/code 0.1.6 → 0.1.8
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 +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP + WebSocket dashboard — mirrors the CLI session in a browser.
|
|
3
|
+
*
|
|
4
|
+
* Uses ONLY Node built-ins (http, crypto, buffer). No npm dependencies.
|
|
5
|
+
* Serves an inline single-page HTML dashboard with a WebSocket-backed event
|
|
6
|
+
* stream showing agent/sub-agent activity, tool calls, handoffs, spawns.
|
|
7
|
+
*
|
|
8
|
+
* Architecture (matching socket-server.mjs):
|
|
9
|
+
* startHttpDashboard({ sessionId, port }) -> { url, broadcastEvent, close }
|
|
10
|
+
* - Registers itself via registerBroadcaster(fn) from event-tap.mjs
|
|
11
|
+
* - Binds to 127.0.0.1 only
|
|
12
|
+
* - WebSocket at /ws (RFC 6455 handshake)
|
|
13
|
+
* - Dashboard HTML served at /
|
|
14
|
+
* - Auth token in URL path for basic access control
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import crypto from 'node:crypto';
|
|
20
|
+
import { registerBroadcaster } from './event-tap.mjs';
|
|
21
|
+
|
|
22
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {string} opts.sessionId
|
|
27
|
+
* @param {number} [opts.port=0] — 0 = OS-assigned
|
|
28
|
+
* @param {string} [opts.secret] — random token for URL path
|
|
29
|
+
* @returns {Promise<{url:string, port:number, broadcastEvent:(obj:any)=>void, close:()=>void}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function startHttpDashboard({ sessionId, port = 0, secret } = {}) {
|
|
32
|
+
const token = secret || crypto.randomBytes(12).toString('hex');
|
|
33
|
+
const pathPrefix = `/dashboard/${token}`;
|
|
34
|
+
|
|
35
|
+
/** @type {Set<import('net').Socket>} */
|
|
36
|
+
const wsClients = new Set();
|
|
37
|
+
|
|
38
|
+
const server = http.createServer((req, res) => {
|
|
39
|
+
// WebSocket upgrade
|
|
40
|
+
if (req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
41
|
+
return handleWebSocketUpgrade(req, res, wsClients);
|
|
42
|
+
}
|
|
43
|
+
// Serve the HTML dashboard
|
|
44
|
+
if (req.url.startsWith(pathPrefix) || req.url === '/') {
|
|
45
|
+
res.writeHead(200, {
|
|
46
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
47
|
+
'Cache-Control': 'no-store',
|
|
48
|
+
});
|
|
49
|
+
res.end(DASHBOARD_HTML(token));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Health endpoint
|
|
53
|
+
if (req.url === '/health') {
|
|
54
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify({ ok: true, sessionId, clients: wsClients.size }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
res.writeHead(404);
|
|
59
|
+
res.end('Not found');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Bind to localhost only
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
server.listen(port, '127.0.0.1', () => {
|
|
65
|
+
const addr = server.address();
|
|
66
|
+
resolve(addr);
|
|
67
|
+
});
|
|
68
|
+
server.once('error', reject);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const addr = server.address();
|
|
72
|
+
const actualPort = addr.port;
|
|
73
|
+
const url = `http://127.0.0.1:${actualPort}/${pathPrefix}`;
|
|
74
|
+
|
|
75
|
+
// Register as event broadcaster so tapSseEvent pushes to us
|
|
76
|
+
const unregister = registerBroadcaster((event) => {
|
|
77
|
+
broadcastEvent(event);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function broadcastEvent(obj) {
|
|
81
|
+
if (wsClients.size === 0) return;
|
|
82
|
+
const frame = encodeWSFrame(JSON.stringify(obj));
|
|
83
|
+
for (const sock of wsClients) {
|
|
84
|
+
try { sock.write(frame); } catch { /* single-client failure doesn't cascade */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function close() {
|
|
89
|
+
unregister();
|
|
90
|
+
for (const sock of wsClients) {
|
|
91
|
+
try { sock.end(); } catch { /* no-op */ }
|
|
92
|
+
}
|
|
93
|
+
wsClients.clear();
|
|
94
|
+
server.close();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { url, port: actualPort, broadcastEvent, close };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── WebSocket helpers (RFC 6455, text frames only) ──
|
|
101
|
+
|
|
102
|
+
function handleWebSocketUpgrade(req, sock, clients) {
|
|
103
|
+
const key = req.headers['sec-websocket-key'];
|
|
104
|
+
if (!key) {
|
|
105
|
+
sock.destroy();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const accept = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
|
|
109
|
+
sock.write(
|
|
110
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
111
|
+
'Upgrade: websocket\r\n' +
|
|
112
|
+
'Connection: Upgrade\r\n' +
|
|
113
|
+
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
114
|
+
'\r\n'
|
|
115
|
+
);
|
|
116
|
+
clients.add(sock);
|
|
117
|
+
sock.once('close', () => clients.delete(sock));
|
|
118
|
+
sock.once('error', () => clients.delete(sock));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function encodeWSFrame(payload) {
|
|
122
|
+
const data = Buffer.from(payload, 'utf-8');
|
|
123
|
+
const len = data.length;
|
|
124
|
+
let header;
|
|
125
|
+
if (len < 126) {
|
|
126
|
+
header = Buffer.alloc(2);
|
|
127
|
+
header[0] = 0x81; // FIN + text opcode
|
|
128
|
+
header[1] = len;
|
|
129
|
+
} else if (len < 65536) {
|
|
130
|
+
header = Buffer.alloc(4);
|
|
131
|
+
header[0] = 0x81;
|
|
132
|
+
header[1] = 126;
|
|
133
|
+
header.writeUInt16BE(len, 2);
|
|
134
|
+
} else {
|
|
135
|
+
header = Buffer.alloc(10);
|
|
136
|
+
header[0] = 0x81;
|
|
137
|
+
header[1] = 127;
|
|
138
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
139
|
+
}
|
|
140
|
+
return Buffer.concat([header, data]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Inline Dashboard HTML ──
|
|
144
|
+
|
|
145
|
+
function DASHBOARD_HTML(token) {
|
|
146
|
+
return `<!DOCTYPE html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="UTF-8">
|
|
150
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
151
|
+
<title>Bahulam Agent Dashboard</title>
|
|
152
|
+
<style>
|
|
153
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
154
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#c9d1d9;
|
|
155
|
+
--dim:#8b949e;--green:#3fb950;--cyan:#58a6ff;--yellow:#d29922;
|
|
156
|
+
--red:#f85149;--brand:#58a6ff}
|
|
157
|
+
body{background:var(--bg);color:var(--text);font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
158
|
+
font-size:13px;line-height:1.5;padding:16px;overflow-y:scroll}
|
|
159
|
+
h1{font-size:16px;font-weight:600;color:var(--brand);margin-bottom:8px;display:flex;align-items:center;gap:8px}
|
|
160
|
+
h1 small{font-size:11px;color:var(--dim);font-weight:400}
|
|
161
|
+
#status{display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap}
|
|
162
|
+
.stat{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:12px}
|
|
163
|
+
.stat .num{color:var(--brand);font-weight:600}
|
|
164
|
+
.stat .lbl{color:var(--dim)}
|
|
165
|
+
.toolbar{margin-bottom:8px;display:flex;gap:6px;flex-wrap:wrap}
|
|
166
|
+
.toolbar button{background:var(--surface);border:1px solid var(--border);color:var(--text);
|
|
167
|
+
padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}
|
|
168
|
+
.toolbar button.active{background:var(--brand);color:#fff;border-color:var(--brand)}
|
|
169
|
+
.toolbar button:hover{background:var(--border)}
|
|
170
|
+
.split{display:flex;gap:12px;height:calc(100vh - 160px)}
|
|
171
|
+
.panel{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:6px;
|
|
172
|
+
overflow-y:auto;padding:4px 0;min-width:0}
|
|
173
|
+
.panel h2{font-size:11px;font-weight:600;color:var(--dim);text-transform:uppercase;
|
|
174
|
+
letter-spacing:0.5px;padding:6px 10px;border-bottom:1px solid var(--border);position:sticky;top:0;
|
|
175
|
+
background:var(--surface);z-index:1}
|
|
176
|
+
.event-line{padding:2px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12px;border-bottom:1px solid #1c2128}
|
|
177
|
+
.event-line:hover{background:#1c2128}
|
|
178
|
+
.tag{display:inline-block;padding:0 5px;border-radius:3px;font-size:10px;font-weight:600;margin-right:4px}
|
|
179
|
+
.tag-spawn{background:#1b3628;color:var(--green)}
|
|
180
|
+
.tag-active{background:#1c2d45;color:var(--cyan)}
|
|
181
|
+
.tag-done{background:#1b3628;color:var(--green)}
|
|
182
|
+
.tag-tool{background:#26242b;color:var(--dim)}
|
|
183
|
+
.tag-handoff{background:#2a2318;color:var(--yellow)}
|
|
184
|
+
.tag-error{background:#2d1b1b;color:var(--red)}
|
|
185
|
+
.time{color:var(--dim);font-size:11px;margin-left:6px}
|
|
186
|
+
.detail{color:var(--dim)}
|
|
187
|
+
::-webkit-scrollbar{width:6px}
|
|
188
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
189
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
190
|
+
</style>
|
|
191
|
+
</head>
|
|
192
|
+
<body>
|
|
193
|
+
<h1>⚡ Agent Dashboard <small id="sessionLabel">connecting…</small></h1>
|
|
194
|
+
<div id="status">
|
|
195
|
+
<div class="stat"><span class="num" id="statEvents">0</span> <span class="lbl">events</span></div>
|
|
196
|
+
<div class="stat"><span class="num" id="statSubAgents">0</span> <span class="lbl">sub-agents</span></div>
|
|
197
|
+
<div class="stat"><span class="num" id="statTools">0</span> <span class="lbl">tools</span></div>
|
|
198
|
+
<div class="stat"><span class="num" id="statHandoffs">0</span> <span class="lbl">handoffs</span></div>
|
|
199
|
+
<div class="stat"><span class="lbl" id="connStatus">● disconnected</span></div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="toolbar">
|
|
202
|
+
<button data-filter="all" class="active">All</button>
|
|
203
|
+
<button data-filter="sub_agent_start,sub_agent_complete">Sub-Agents</button>
|
|
204
|
+
<button data-filter="delegation">Handoffs</button>
|
|
205
|
+
<button data-filter="tool_call,tool_done">Tools</button>
|
|
206
|
+
<button data-filter="error">Errors</button>
|
|
207
|
+
<button id="btnAutoScroll" class="active">Auto-scroll</button>
|
|
208
|
+
<button id="btnClear">Clear</button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="split">
|
|
211
|
+
<div class="panel" id="eventLog"><h2>Event Log</h2></div>
|
|
212
|
+
<div class="panel" id="subAgentTree"><h2>Sub-Agent Activity</h2></div>
|
|
213
|
+
</div>
|
|
214
|
+
<script>
|
|
215
|
+
(function(){
|
|
216
|
+
const token = '${token}';
|
|
217
|
+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
218
|
+
const wsUrl = wsProto + '//' + location.host + '/ws';
|
|
219
|
+
let ws;
|
|
220
|
+
let filters = ['all'];
|
|
221
|
+
let autoScroll = true;
|
|
222
|
+
let eventCount = 0;
|
|
223
|
+
let subAgentCount = 0;
|
|
224
|
+
let toolCount = 0;
|
|
225
|
+
let handoffCount = 0;
|
|
226
|
+
// Sub-agent state: id -> { id, type, query, startTime, tools, status }
|
|
227
|
+
const subAgents = new Map();
|
|
228
|
+
|
|
229
|
+
function connect() {
|
|
230
|
+
ws = new WebSocket(wsUrl);
|
|
231
|
+
ws.onopen = () => { document.getElementById('connStatus').textContent = '● connected'; };
|
|
232
|
+
ws.onclose = () => {
|
|
233
|
+
document.getElementById('connStatus').textContent = '● disconnected';
|
|
234
|
+
setTimeout(connect, 3000);
|
|
235
|
+
};
|
|
236
|
+
ws.onmessage = (msg) => {
|
|
237
|
+
try {
|
|
238
|
+
const ev = JSON.parse(msg.data);
|
|
239
|
+
handleEvent(ev);
|
|
240
|
+
} catch(e) { /* ignore parse errors */ }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleEvent(ev) {
|
|
245
|
+
eventCount++;
|
|
246
|
+
updateStats();
|
|
247
|
+
const line = renderEventLine(ev);
|
|
248
|
+
if (line) appendEvent(line);
|
|
249
|
+
updateSubAgentTree(ev);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderEventLine(ev) {
|
|
253
|
+
const type = ev.type || 'unknown';
|
|
254
|
+
const data = ev.data || {};
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
let tag, label, detail = '';
|
|
257
|
+
|
|
258
|
+
switch(type) {
|
|
259
|
+
case 'sub_agent_start':
|
|
260
|
+
tag = '<span class="tag tag-spawn">spawn</span>';
|
|
261
|
+
label = (data.type || 'agent') + (data.query ? ' "' + data.query.slice(0, 60) + '"' : '');
|
|
262
|
+
subAgentCount++; toolCount++; break;
|
|
263
|
+
case 'sub_agent_complete':
|
|
264
|
+
tag = '<span class="tag tag-done">done</span>';
|
|
265
|
+
label = (data.type || 'agent');
|
|
266
|
+
detail = data.tool_calls != null ? ' · ' + data.tool_calls + ' tools' : '';
|
|
267
|
+
if (data.duration_s) detail += ' · ' + data.duration_s.toFixed(1) + 's';
|
|
268
|
+
break;
|
|
269
|
+
case 'delegation':
|
|
270
|
+
tag = '<span class="tag tag-handoff">handoff</span>';
|
|
271
|
+
label = (data.from || '?') + ' → ' + (data.to || '?');
|
|
272
|
+
if (data.instruction) detail = ' "' + data.instruction.slice(0, 50) + '"';
|
|
273
|
+
handoffCount++; break;
|
|
274
|
+
case 'tool_call':
|
|
275
|
+
case 'tool_request':
|
|
276
|
+
tag = '<span class="tag tag-tool">▶ tool</span>';
|
|
277
|
+
label = (data.tool || '?') + (data.args && data.args.file_path ? ' ' + data.args.file_path : '');
|
|
278
|
+
toolCount++; break;
|
|
279
|
+
case 'tool_done':
|
|
280
|
+
case 'tool_result':
|
|
281
|
+
tag = '<span class="tag tag-tool">✓ tool</span>';
|
|
282
|
+
label = (data.tool || '?');
|
|
283
|
+
if (data.duration_s) detail = ' · ' + data.duration_s.toFixed(1) + 's';
|
|
284
|
+
break;
|
|
285
|
+
case 'error':
|
|
286
|
+
tag = '<span class="tag tag-error">error</span>';
|
|
287
|
+
label = (data.message || '').slice(0, 80);
|
|
288
|
+
break;
|
|
289
|
+
case 'complete':
|
|
290
|
+
tag = '<span class="tag tag-done">complete</span>';
|
|
291
|
+
label = 'Agent finished';
|
|
292
|
+
if (data.tool_calls) detail = ' · ' + data.tool_calls + ' tools';
|
|
293
|
+
break;
|
|
294
|
+
case 'thinking':
|
|
295
|
+
return null; // too noisy
|
|
296
|
+
case 'content':
|
|
297
|
+
case 'content_partial':
|
|
298
|
+
return null;
|
|
299
|
+
default:
|
|
300
|
+
tag = '<span class="tag tag-active">' + type.slice(0, 8) + '</span>';
|
|
301
|
+
label = type;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
eventCount++;
|
|
305
|
+
updateStats();
|
|
306
|
+
return '<div class="event-line">' + tag + esc(label) + '<span class="detail">' + esc(detail) + '</span></div>';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function appendEvent(html) {
|
|
310
|
+
if (!html) return;
|
|
311
|
+
const log = document.getElementById('eventLog');
|
|
312
|
+
log.insertAdjacentHTML('beforeend', html);
|
|
313
|
+
if (autoScroll) log.scrollTop = log.scrollHeight;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function updateSubAgentTree(ev) {
|
|
317
|
+
const type = ev.type;
|
|
318
|
+
const data = ev.data || {};
|
|
319
|
+
const container = document.getElementById('subAgentTree');
|
|
320
|
+
let changed = false;
|
|
321
|
+
|
|
322
|
+
if (type === 'sub_agent_start') {
|
|
323
|
+
const id = data.id || 'sa-' + Date.now();
|
|
324
|
+
subAgents.set(id, { id, type: data.type || 'agent', query: data.query || '', startTime: Date.now(), tools: 0, status: 'active' });
|
|
325
|
+
changed = true;
|
|
326
|
+
} else if (type === 'sub_agent_complete') {
|
|
327
|
+
const id = data.id;
|
|
328
|
+
if (id && subAgents.has(id)) {
|
|
329
|
+
const sa = subAgents.get(id);
|
|
330
|
+
sa.status = 'done';
|
|
331
|
+
sa.tools = data.tool_calls || sa.tools;
|
|
332
|
+
changed = true;
|
|
333
|
+
}
|
|
334
|
+
} else if (type === 'sub_agent_tool') {
|
|
335
|
+
const agentType = data.type || '';
|
|
336
|
+
// Find the most recent active sub-agent of this type
|
|
337
|
+
let found = null;
|
|
338
|
+
for (const [id, sa] of subAgents) {
|
|
339
|
+
if (sa.type === agentType && sa.status === 'active') { found = sa; }
|
|
340
|
+
}
|
|
341
|
+
if (found) { found.tools++; changed = true; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (changed) {
|
|
345
|
+
let html = '<h2>Sub-Agent Activity</h2>';
|
|
346
|
+
if (subAgents.size === 0) { html += '<div class="event-line" style="color:var(--dim)">(none yet)</div>'; }
|
|
347
|
+
else {
|
|
348
|
+
for (const [id, sa] of subAgents) {
|
|
349
|
+
const elapsed = ((Date.now() - sa.startTime) / 1000).toFixed(1);
|
|
350
|
+
const statusTag = sa.status === 'active'
|
|
351
|
+
? '<span class="tag tag-active">active</span>'
|
|
352
|
+
: '<span class="tag tag-done">done</span>';
|
|
353
|
+
html += '<div class="event-line">' + statusTag + ' <b>' + esc(sa.type) + '</b>';
|
|
354
|
+
if (sa.query) html += ' ' + esc(sa.query.slice(0, 40));
|
|
355
|
+
html += ' · <span class="time">' + sa.tools + ' tools · ' + elapsed + 's</span></div>';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
container.innerHTML = html;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function updateStats() {
|
|
363
|
+
document.getElementById('statEvents').textContent = eventCount;
|
|
364
|
+
document.getElementById('statSubAgents').textContent = subAgents.size;
|
|
365
|
+
document.getElementById('statTools').textContent = toolCount;
|
|
366
|
+
document.getElementById('statHandoffs').textContent = handoffCount;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function esc(s) { return String(s || '').replace(/[&<>"]/g, function(m) {
|
|
370
|
+
return {'&':'&','<':'<','>':'>','"':'"'}[m]; });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Toolbar filter buttons
|
|
374
|
+
document.querySelectorAll('[data-filter]').forEach(btn => {
|
|
375
|
+
btn.addEventListener('click', () => {
|
|
376
|
+
document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active'));
|
|
377
|
+
btn.classList.add('active');
|
|
378
|
+
filters = btn.dataset.filter === 'all' ? ['all'] : btn.dataset.filter.split(',');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
document.getElementById('btnAutoScroll').addEventListener('click', function() {
|
|
383
|
+
autoScroll = !autoScroll;
|
|
384
|
+
this.classList.toggle('active');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
document.getElementById('btnClear').addEventListener('click', () => {
|
|
388
|
+
document.getElementById('eventLog').innerHTML = '<h2>Event Log</h2>';
|
|
389
|
+
document.getElementById('subAgentTree').innerHTML = '<h2>Sub-Agent Activity</h2>';
|
|
390
|
+
subAgents.clear();
|
|
391
|
+
eventCount = 0; subAgentCount = 0; toolCount = 0; handoffCount = 0;
|
|
392
|
+
updateStats();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
connect();
|
|
396
|
+
})();
|
|
397
|
+
</script>
|
|
398
|
+
</body>
|
|
399
|
+
</html>`;
|
|
400
|
+
}
|