@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.
Files changed (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. 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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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
+ }
@@ -58,6 +58,7 @@ export const DAEMON_OWNED_FIELDS = Object.freeze([
58
58
  'user',
59
59
  'model',
60
60
  'modelLimits',
61
+ 'subAgentModels',
61
62
  'blockedOps',
62
63
  'delegations',
63
64
  'phases',