@bahulam/code 0.1.5 → 0.1.7

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