@archznn/crewloop-skills 0.3.0 → 0.4.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.
Files changed (164) hide show
  1. package/package.json +16 -3
  2. package/packages/cli/README.md +55 -0
  3. package/packages/cli/bin/crewloop.js +6 -0
  4. package/packages/cli/dist/agents.d.ts +7 -0
  5. package/packages/cli/dist/agents.d.ts.map +1 -0
  6. package/packages/cli/dist/agents.js +31 -0
  7. package/packages/cli/dist/agents.js.map +1 -0
  8. package/packages/cli/dist/cli.d.ts +14 -0
  9. package/packages/cli/dist/cli.d.ts.map +1 -0
  10. package/packages/cli/dist/cli.js +255 -0
  11. package/packages/cli/dist/cli.js.map +1 -0
  12. package/packages/cli/dist/installer.d.ts +19 -0
  13. package/packages/cli/dist/installer.d.ts.map +1 -0
  14. package/packages/cli/dist/installer.js +89 -0
  15. package/packages/cli/dist/installer.js.map +1 -0
  16. package/packages/cli/dist/mcp.d.ts +20 -0
  17. package/packages/cli/dist/mcp.d.ts.map +1 -0
  18. package/packages/cli/dist/mcp.js +130 -0
  19. package/packages/cli/dist/mcp.js.map +1 -0
  20. package/packages/cli/dist/resolver.d.ts +7 -0
  21. package/packages/cli/dist/resolver.d.ts.map +1 -0
  22. package/packages/cli/dist/resolver.js +57 -0
  23. package/packages/cli/dist/resolver.js.map +1 -0
  24. package/packages/cli/dist/tests/agents.test.d.ts +2 -0
  25. package/packages/cli/dist/tests/agents.test.d.ts.map +1 -0
  26. package/packages/cli/dist/tests/agents.test.js +27 -0
  27. package/packages/cli/dist/tests/agents.test.js.map +1 -0
  28. package/packages/cli/dist/tests/cli.test.d.ts +2 -0
  29. package/packages/cli/dist/tests/cli.test.d.ts.map +1 -0
  30. package/packages/cli/dist/tests/cli.test.js +103 -0
  31. package/packages/cli/dist/tests/cli.test.js.map +1 -0
  32. package/packages/cli/dist/tests/installer.test.d.ts +2 -0
  33. package/packages/cli/dist/tests/installer.test.d.ts.map +1 -0
  34. package/packages/cli/dist/tests/installer.test.js +129 -0
  35. package/packages/cli/dist/tests/installer.test.js.map +1 -0
  36. package/packages/cli/dist/tests/mcp.test.d.ts +2 -0
  37. package/packages/cli/dist/tests/mcp.test.d.ts.map +1 -0
  38. package/packages/cli/dist/tests/mcp.test.js +153 -0
  39. package/packages/cli/dist/tests/mcp.test.js.map +1 -0
  40. package/packages/cli/dist/tests/resolver.test.d.ts +2 -0
  41. package/packages/cli/dist/tests/resolver.test.d.ts.map +1 -0
  42. package/packages/cli/dist/tests/resolver.test.js +41 -0
  43. package/packages/cli/dist/tests/resolver.test.js.map +1 -0
  44. package/servers/dashboard/README.md +87 -0
  45. package/servers/dashboard/bin/crewloop-dashboard.js +5 -0
  46. package/servers/dashboard/config-examples/codex-hooks.json +14 -0
  47. package/servers/dashboard/config-examples/kimi-code-config.toml +6 -0
  48. package/servers/dashboard/config-examples/opencode-plugin/crewloop-dashboard.js +64 -0
  49. package/servers/dashboard/dist/adapters/codex.d.ts +23 -0
  50. package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -0
  51. package/servers/dashboard/dist/adapters/codex.js +28 -0
  52. package/servers/dashboard/dist/adapters/codex.js.map +1 -0
  53. package/servers/dashboard/dist/adapters/kimi.d.ts +13 -0
  54. package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -0
  55. package/servers/dashboard/dist/adapters/kimi.js +28 -0
  56. package/servers/dashboard/dist/adapters/kimi.js.map +1 -0
  57. package/servers/dashboard/dist/adapters/opencode.d.ts +9 -0
  58. package/servers/dashboard/dist/adapters/opencode.d.ts.map +1 -0
  59. package/servers/dashboard/dist/adapters/opencode.js +26 -0
  60. package/servers/dashboard/dist/adapters/opencode.js.map +1 -0
  61. package/servers/dashboard/dist/adapters/shim.d.ts +7 -0
  62. package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -0
  63. package/servers/dashboard/dist/adapters/shim.js +107 -0
  64. package/servers/dashboard/dist/adapters/shim.js.map +1 -0
  65. package/servers/dashboard/dist/adapters/shim.test.d.ts +2 -0
  66. package/servers/dashboard/dist/adapters/shim.test.d.ts.map +1 -0
  67. package/servers/dashboard/dist/adapters/shim.test.js +72 -0
  68. package/servers/dashboard/dist/adapters/shim.test.js.map +1 -0
  69. package/servers/dashboard/dist/api/event.d.ts +13 -0
  70. package/servers/dashboard/dist/api/event.d.ts.map +1 -0
  71. package/servers/dashboard/dist/api/event.js +52 -0
  72. package/servers/dashboard/dist/api/event.js.map +1 -0
  73. package/servers/dashboard/dist/api/skills.d.ts +4 -0
  74. package/servers/dashboard/dist/api/skills.d.ts.map +1 -0
  75. package/servers/dashboard/dist/api/skills.js +12 -0
  76. package/servers/dashboard/dist/api/skills.js.map +1 -0
  77. package/servers/dashboard/dist/config.d.ts +11 -0
  78. package/servers/dashboard/dist/config.d.ts.map +1 -0
  79. package/servers/dashboard/dist/config.js +65 -0
  80. package/servers/dashboard/dist/config.js.map +1 -0
  81. package/servers/dashboard/dist/filters/sanitize.d.ts +14 -0
  82. package/servers/dashboard/dist/filters/sanitize.d.ts.map +1 -0
  83. package/servers/dashboard/dist/filters/sanitize.js +64 -0
  84. package/servers/dashboard/dist/filters/sanitize.js.map +1 -0
  85. package/servers/dashboard/dist/filters/sanitize.test.d.ts +2 -0
  86. package/servers/dashboard/dist/filters/sanitize.test.d.ts.map +1 -0
  87. package/servers/dashboard/dist/filters/sanitize.test.js +71 -0
  88. package/servers/dashboard/dist/filters/sanitize.test.js.map +1 -0
  89. package/servers/dashboard/dist/index.d.ts +2 -0
  90. package/servers/dashboard/dist/index.d.ts.map +1 -0
  91. package/servers/dashboard/dist/index.js +22 -0
  92. package/servers/dashboard/dist/index.js.map +1 -0
  93. package/servers/dashboard/dist/presenter.d.ts +7 -0
  94. package/servers/dashboard/dist/presenter.d.ts.map +1 -0
  95. package/servers/dashboard/dist/presenter.js +54 -0
  96. package/servers/dashboard/dist/presenter.js.map +1 -0
  97. package/servers/dashboard/dist/presenter.test.d.ts +2 -0
  98. package/servers/dashboard/dist/presenter.test.d.ts.map +1 -0
  99. package/servers/dashboard/dist/presenter.test.js +66 -0
  100. package/servers/dashboard/dist/presenter.test.js.map +1 -0
  101. package/servers/dashboard/dist/server.d.ts +13 -0
  102. package/servers/dashboard/dist/server.d.ts.map +1 -0
  103. package/servers/dashboard/dist/server.js +162 -0
  104. package/servers/dashboard/dist/server.js.map +1 -0
  105. package/servers/dashboard/dist/server.test.d.ts +2 -0
  106. package/servers/dashboard/dist/server.test.d.ts.map +1 -0
  107. package/servers/dashboard/dist/server.test.js +113 -0
  108. package/servers/dashboard/dist/server.test.js.map +1 -0
  109. package/servers/dashboard/dist/skills/infer.d.ts +8 -0
  110. package/servers/dashboard/dist/skills/infer.d.ts.map +1 -0
  111. package/servers/dashboard/dist/skills/infer.js +48 -0
  112. package/servers/dashboard/dist/skills/infer.js.map +1 -0
  113. package/servers/dashboard/dist/skills/infer.test.d.ts +2 -0
  114. package/servers/dashboard/dist/skills/infer.test.d.ts.map +1 -0
  115. package/servers/dashboard/dist/skills/infer.test.js +82 -0
  116. package/servers/dashboard/dist/skills/infer.test.js.map +1 -0
  117. package/servers/dashboard/dist/skills/mapping.d.ts +5 -0
  118. package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -0
  119. package/servers/dashboard/dist/skills/mapping.js +28 -0
  120. package/servers/dashboard/dist/skills/mapping.js.map +1 -0
  121. package/servers/dashboard/dist/skills/registry.d.ts +11 -0
  122. package/servers/dashboard/dist/skills/registry.d.ts.map +1 -0
  123. package/servers/dashboard/dist/skills/registry.js +59 -0
  124. package/servers/dashboard/dist/skills/registry.js.map +1 -0
  125. package/servers/dashboard/dist/state.d.ts +18 -0
  126. package/servers/dashboard/dist/state.d.ts.map +1 -0
  127. package/servers/dashboard/dist/state.js +91 -0
  128. package/servers/dashboard/dist/state.js.map +1 -0
  129. package/servers/dashboard/dist/state.test.d.ts +2 -0
  130. package/servers/dashboard/dist/state.test.d.ts.map +1 -0
  131. package/servers/dashboard/dist/state.test.js +83 -0
  132. package/servers/dashboard/dist/state.test.js.map +1 -0
  133. package/servers/dashboard/dist/types.d.ts +86 -0
  134. package/servers/dashboard/dist/types.d.ts.map +1 -0
  135. package/servers/dashboard/dist/types.js +3 -0
  136. package/servers/dashboard/dist/types.js.map +1 -0
  137. package/servers/dashboard/package.json +46 -0
  138. package/servers/dashboard/public/app.js +447 -0
  139. package/servers/dashboard/public/index.html +96 -0
  140. package/servers/dashboard/public/styles.css +664 -0
  141. package/servers/dashboard/src/adapters/codex.ts +50 -0
  142. package/servers/dashboard/src/adapters/kimi.ts +40 -0
  143. package/servers/dashboard/src/adapters/opencode.ts +36 -0
  144. package/servers/dashboard/src/adapters/shim.test.ts +74 -0
  145. package/servers/dashboard/src/adapters/shim.ts +120 -0
  146. package/servers/dashboard/src/api/event.ts +70 -0
  147. package/servers/dashboard/src/api/skills.ts +11 -0
  148. package/servers/dashboard/src/config.ts +66 -0
  149. package/servers/dashboard/src/filters/sanitize.test.ts +94 -0
  150. package/servers/dashboard/src/filters/sanitize.ts +78 -0
  151. package/servers/dashboard/src/index.ts +24 -0
  152. package/servers/dashboard/src/presenter.test.ts +69 -0
  153. package/servers/dashboard/src/presenter.ts +56 -0
  154. package/servers/dashboard/src/server.test.ts +123 -0
  155. package/servers/dashboard/src/server.ts +191 -0
  156. package/servers/dashboard/src/skills/infer.test.ts +86 -0
  157. package/servers/dashboard/src/skills/infer.ts +53 -0
  158. package/servers/dashboard/src/skills/mapping.ts +26 -0
  159. package/servers/dashboard/src/skills/registry.ts +60 -0
  160. package/servers/dashboard/src/state.test.ts +88 -0
  161. package/servers/dashboard/src/state.ts +115 -0
  162. package/servers/dashboard/src/types.ts +110 -0
  163. package/servers/dashboard/tsconfig.json +19 -0
  164. package/skills/orchestrator/SKILL.md +1 -1
@@ -0,0 +1,447 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ // ---- Config ----
5
+ const WS_URL = `ws://${location.host}/ws`;
6
+ const MAX_EVENTS = 100;
7
+ const SKILL_ICONS = {
8
+ orchestrator: 'ph-target',
9
+ architect: 'ph-blueprint',
10
+ designer: 'ph-palette',
11
+ engineer: 'ph-wrench',
12
+ reviewer: 'ph-magnifying-glass',
13
+ shipper: 'ph-rocket-launch',
14
+ 'docs-writer': 'ph-article',
15
+ tester: 'ph-flask',
16
+ 'product-manager': 'ph-chart-bar',
17
+ maintainer: 'ph-toolbox',
18
+ researcher: 'ph-microscope',
19
+ 'security-guard': 'ph-shield',
20
+ 'accessibility-auditor': 'ph-person',
21
+ 'obsidian-second-brain': 'ph-brain',
22
+ 'project-mapper': 'ph-tree-structure',
23
+ default: 'ph-circle',
24
+ };
25
+
26
+ // ---- State ----
27
+ const state = {
28
+ sessions: new Map(),
29
+ activeSessionId: null,
30
+ selectedSessionId: null,
31
+ theme: localStorage.getItem('crewloop-theme') || 'system',
32
+ connection: 'connecting',
33
+ lastPong: 0,
34
+ eventCountWindow: [],
35
+ ws: null,
36
+ reconnectTimer: null,
37
+ pingTimer: null,
38
+ };
39
+
40
+ // ---- DOM refs ----
41
+ const $ = (id) => document.getElementById(id);
42
+ const themeToggle = $('theme-toggle');
43
+ const connectionDot = $('connection-dot');
44
+ const sessionTrigger = $('session-trigger');
45
+ const sessionLabel = $('session-label');
46
+ const sessionList = $('session-list');
47
+ const activeStrip = $('active-strip');
48
+ const activeSkillIcon = $('active-skill-icon');
49
+ const activeSkillName = $('active-skill-name');
50
+ const statusBadge = $('status-badge');
51
+ const statusDot = $('status-dot');
52
+ const statusText = $('status-text');
53
+ const confidenceBadge = $('confidence-badge');
54
+ const activeSkillSource = $('active-skill-source');
55
+ const toolCount = $('tool-count');
56
+ const durationEl = $('duration');
57
+ const eventRate = $('event-rate');
58
+ const timeline = $('timeline');
59
+ const activityGraph = $('activity-graph');
60
+
61
+ // ---- Utils ----
62
+ function formatDuration(ms) {
63
+ const totalSeconds = Math.floor(ms / 1000);
64
+ const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
65
+ const seconds = (totalSeconds % 60).toString().padStart(2, '0');
66
+ if (totalSeconds < 3600) return `${minutes}:${seconds}`;
67
+ const hours = Math.floor(totalSeconds / 3600);
68
+ return `${hours}:${minutes}:${seconds}`;
69
+ }
70
+
71
+ function formatTime(ts) {
72
+ const d = new Date(ts);
73
+ return d.toLocaleTimeString(undefined, { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
74
+ }
75
+
76
+ function updateEventRate() {
77
+ const now = Date.now();
78
+ const windowStart = now - 60000;
79
+ state.eventCountWindow = state.eventCountWindow.filter((t) => t > windowStart);
80
+ eventRate.textContent = state.eventCountWindow.length.toString();
81
+ }
82
+
83
+ function skillIcon(skillName) {
84
+ const key = skillName?.toLowerCase().replace(/\s+/g, '-');
85
+ return SKILL_ICONS[key] || SKILL_ICONS.default;
86
+ }
87
+
88
+ function prefersReducedMotion() {
89
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
90
+ }
91
+
92
+ // ---- Theme ----
93
+ function applyTheme(theme) {
94
+ const root = document.documentElement;
95
+ let resolved = theme;
96
+ if (theme === 'system') {
97
+ resolved = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
98
+ }
99
+ root.setAttribute('data-theme', resolved);
100
+ const icon = themeToggle.querySelector('i');
101
+ if (resolved === 'light') {
102
+ icon.className = 'ph ph-sun';
103
+ } else {
104
+ icon.className = 'ph ph-moon';
105
+ }
106
+ }
107
+
108
+ function cycleTheme() {
109
+ const order = ['system', 'light', 'dark'];
110
+ const idx = order.indexOf(state.theme);
111
+ state.theme = order[(idx + 1) % order.length];
112
+ localStorage.setItem('crewloop-theme', state.theme);
113
+ applyTheme(state.theme);
114
+ }
115
+
116
+ // ---- WebSocket ----
117
+ function setConnection(status) {
118
+ state.connection = status;
119
+ connectionDot.className = 'connection-dot ' + status;
120
+ }
121
+
122
+ function connect() {
123
+ setConnection('connecting');
124
+ const ws = new WebSocket(WS_URL);
125
+ state.ws = ws;
126
+
127
+ ws.addEventListener('open', () => {
128
+ setConnection('connected');
129
+ state.lastPong = Date.now();
130
+ startPing();
131
+ });
132
+
133
+ ws.addEventListener('message', (event) => {
134
+ let msg;
135
+ try {
136
+ msg = JSON.parse(event.data);
137
+ } catch {
138
+ return;
139
+ }
140
+ handleMessage(msg);
141
+ });
142
+
143
+ ws.addEventListener('close', () => {
144
+ setConnection('disconnected');
145
+ stopPing();
146
+ scheduleReconnect();
147
+ });
148
+
149
+ ws.addEventListener('error', () => {
150
+ ws.close();
151
+ });
152
+ }
153
+
154
+ function scheduleReconnect() {
155
+ if (state.reconnectTimer) return;
156
+ state.reconnectTimer = setTimeout(() => {
157
+ state.reconnectTimer = null;
158
+ connect();
159
+ }, 3000);
160
+ }
161
+
162
+ function startPing() {
163
+ stopPing();
164
+ state.pingTimer = setInterval(() => {
165
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
166
+ state.ws.send(JSON.stringify({ type: 'ping' }));
167
+ if (Date.now() - state.lastPong > 35000) {
168
+ state.ws.close();
169
+ }
170
+ }, 15000);
171
+ }
172
+
173
+ function stopPing() {
174
+ if (state.pingTimer) {
175
+ clearInterval(state.pingTimer);
176
+ state.pingTimer = null;
177
+ }
178
+ }
179
+
180
+ function handleMessage(msg) {
181
+ if (msg.type === 'pong') {
182
+ state.lastPong = Date.now();
183
+ return;
184
+ }
185
+ if (msg.type === 'snapshot') {
186
+ state.sessions.clear();
187
+ (msg.sessions || []).forEach((s) => state.sessions.set(s.id, s));
188
+ if (!state.selectedSessionId) {
189
+ state.selectedSessionId = state.activeSessionId || state.sessions.keys().next().value || null;
190
+ }
191
+ renderSessionSelector();
192
+ renderAll();
193
+ return;
194
+ }
195
+ if (msg.type === 'update') {
196
+ const session = msg.session;
197
+ if (!session) return;
198
+ state.sessions.set(session.id, session);
199
+ if (msg.isActive) {
200
+ state.activeSessionId = session.id;
201
+ if (!state.selectedSessionId) state.selectedSessionId = session.id;
202
+ }
203
+ state.eventCountWindow.push(Date.now());
204
+ renderSessionSelector();
205
+ renderAll();
206
+ }
207
+ }
208
+
209
+ // ---- Rendering ----
210
+ function getSession() {
211
+ return state.sessions.get(state.selectedSessionId) || null;
212
+ }
213
+
214
+ function renderAll() {
215
+ const session = getSession();
216
+ renderActiveSkill(session);
217
+ renderTelemetry(session);
218
+ renderTimeline(session);
219
+ renderActivityGraph(session);
220
+ }
221
+
222
+ function renderActiveSkill(session) {
223
+ if (!session) {
224
+ activeSkillName.textContent = 'IDLE';
225
+ activeSkillIcon.className = 'ph ph-circle';
226
+ activeStrip.className = 'panel-accent-strip';
227
+ statusDot.className = 'status-dot';
228
+ statusText.textContent = 'IDLE';
229
+ confidenceBadge.textContent = 'unknown';
230
+ activeSkillSource.innerHTML = '<i class="ph ph-monitor"></i><span>waiting for events</span>';
231
+ return;
232
+ }
233
+
234
+ const skill = session.activeSkill || { name: 'UNKNOWN', confidence: 'low' };
235
+ const iconClass = skillIcon(skill.name);
236
+ activeSkillName.textContent = skill.name.toUpperCase();
237
+ activeSkillIcon.className = 'ph ' + iconClass;
238
+
239
+ const running = session.status === 'running';
240
+ activeStrip.className = 'panel-accent-strip' + (running ? ' running' : '');
241
+ statusDot.className = 'status-dot ' + (running ? 'running' : session.status || '');
242
+ statusText.textContent = running ? 'RUNNING' : (session.status ? session.status.toUpperCase() : 'IDLE');
243
+ confidenceBadge.textContent = skill.confidence || 'low';
244
+
245
+ activeSkillSource.innerHTML = `<i class="ph ph-${sourceIcon(session.source)}"></i><span>${session.source || 'unknown'}</span>`;
246
+ }
247
+
248
+ function sourceIcon(source) {
249
+ switch (source) {
250
+ case 'kimi': return 'chat-teardrop-text';
251
+ case 'codex': return 'terminal';
252
+ case 'opencode': return 'code-block';
253
+ case 'log-watcher': return 'file-text';
254
+ default: return 'monitor';
255
+ }
256
+ }
257
+
258
+ function renderTelemetry(session) {
259
+ if (!session) {
260
+ toolCount.textContent = '0';
261
+ durationEl.textContent = '00:00';
262
+ updateEventRate();
263
+ return;
264
+ }
265
+ const events = session.events || [];
266
+ const toolEvents = events.filter((e) => e.event_type === 'tool_start' || e.event_type === 'tool_end');
267
+ toolCount.textContent = String(Math.ceil(toolEvents.length / 2));
268
+ const dur = session.lastActivity && session.startTime ? session.lastActivity - session.startTime : 0;
269
+ durationEl.textContent = formatDuration(dur);
270
+ updateEventRate();
271
+ }
272
+
273
+ function renderTimeline(session) {
274
+ timeline.innerHTML = '';
275
+ if (!session) return;
276
+ const events = (session.events || []).slice(-MAX_EVENTS).reverse();
277
+ const fragment = document.createDocumentFragment();
278
+ events.forEach((ev) => {
279
+ const li = document.createElement('li');
280
+ li.className = 'timeline-item';
281
+ const outcome = ev.status || (ev.event_type.endsWith('_end') ? 'success' : '');
282
+ li.innerHTML = `
283
+ <span class="timeline-time">${formatTime(ev.timestamp)}</span>
284
+ <span class="timeline-dot ${outcome}"></span>
285
+ <div class="timeline-main">
286
+ <span class="timeline-tool">${ev.tool || ev.event_type}</span>
287
+ <span class="timeline-detail">${escapeHtml(ev.detail || ev.skill || '')}</span>
288
+ </div>
289
+ <span class="timeline-outcome">${outcome}</span>
290
+ `;
291
+ fragment.appendChild(li);
292
+ });
293
+ timeline.appendChild(fragment);
294
+ }
295
+
296
+ function escapeHtml(str) {
297
+ return String(str)
298
+ .replace(/&/g, '&amp;')
299
+ .replace(/</g, '&lt;')
300
+ .replace(/>/g, '&gt;')
301
+ .replace(/"/g, '&quot;')
302
+ .replace(/'/g, '&#039;');
303
+ }
304
+
305
+ function renderActivityGraph(session) {
306
+ activityGraph.innerHTML = '';
307
+ if (!session || !session.events || session.events.length === 0) {
308
+ activityGraph.innerHTML = '<div class="activity-empty">Waiting for agent activity...</div>';
309
+ return;
310
+ }
311
+
312
+ const events = session.events;
313
+ const now = Date.now();
314
+ const span = Math.max(60000, now - Math.min(...events.map((e) => e.timestamp)));
315
+ const buckets = 40;
316
+ const bucketMs = span / buckets;
317
+ const counts = new Array(buckets).fill(0);
318
+
319
+ events.forEach((e) => {
320
+ const idx = Math.min(buckets - 1, Math.floor((now - e.timestamp) / bucketMs));
321
+ const safeIdx = buckets - 1 - idx;
322
+ counts[safeIdx]++;
323
+ });
324
+
325
+ const max = Math.max(1, ...counts);
326
+ const canvas = document.createElement('canvas');
327
+ canvas.className = 'activity-canvas';
328
+ const rect = activityGraph.getBoundingClientRect();
329
+ const dpr = window.devicePixelRatio || 1;
330
+ canvas.width = Math.max(1, Math.floor(rect.width * dpr));
331
+ canvas.height = Math.max(1, Math.floor(rect.height * dpr));
332
+ const ctx = canvas.getContext('2d');
333
+ ctx.scale(dpr, dpr);
334
+ const width = rect.width;
335
+ const height = rect.height;
336
+ const pad = 4;
337
+ const barW = (width - pad * 2) / buckets;
338
+
339
+ const style = getComputedStyle(document.documentElement);
340
+ const accent = style.getPropertyValue('--accent').trim() || '#22d3ee';
341
+ const muted = style.getPropertyValue('--bg-inset').trim() || '#1c1c1f';
342
+
343
+ // background grid
344
+ ctx.fillStyle = muted;
345
+ ctx.fillRect(0, 0, width, height);
346
+
347
+ counts.forEach((count, i) => {
348
+ const barH = (count / max) * (height - pad * 2);
349
+ const x = pad + i * barW;
350
+ const y = height - pad - barH;
351
+ ctx.fillStyle = accent;
352
+ ctx.fillRect(x + 1, y, Math.max(1, barW - 2), Math.max(1, barH));
353
+ });
354
+
355
+ activityGraph.appendChild(canvas);
356
+ }
357
+
358
+ function renderSessionSelector() {
359
+ sessionList.innerHTML = '';
360
+ if (state.sessions.size === 0) {
361
+ sessionLabel.textContent = 'No session';
362
+ return;
363
+ }
364
+
365
+ const sessions = Array.from(state.sessions.values()).sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
366
+ if (!state.selectedSessionId || !state.sessions.has(state.selectedSessionId)) {
367
+ state.selectedSessionId = sessions[0].id;
368
+ }
369
+
370
+ const activeSession = state.sessions.get(state.activeSessionId);
371
+ let label = 'No session';
372
+ const current = state.sessions.get(state.selectedSessionId);
373
+ if (current) {
374
+ label = current.skill ? current.skill.toUpperCase() : truncate(current.id, 12);
375
+ if (current.id === state.activeSessionId && activeSession) {
376
+ label = '● ' + label;
377
+ }
378
+ }
379
+ sessionLabel.textContent = label;
380
+
381
+ sessions.forEach((s) => {
382
+ const li = document.createElement('li');
383
+ li.className = 'session-item' + (s.id === state.selectedSessionId ? ' active' : '');
384
+ li.setAttribute('role', 'option');
385
+ li.setAttribute('aria-selected', String(s.id === state.selectedSessionId));
386
+ li.innerHTML = `
387
+ <span class="session-item-id">${truncate(s.id, 16)}</span>
388
+ <span class="session-item-source">${s.source || 'unknown'}</span>
389
+ `;
390
+ li.addEventListener('click', () => {
391
+ state.selectedSessionId = s.id;
392
+ closeSessionList();
393
+ renderAll();
394
+ });
395
+ sessionList.appendChild(li);
396
+ });
397
+ }
398
+
399
+ function truncate(str, n) {
400
+ if (!str) return '';
401
+ return str.length > n ? str.slice(0, n - 1) + '…' : str;
402
+ }
403
+
404
+ function toggleSessionList() {
405
+ const open = sessionList.classList.toggle('open');
406
+ sessionTrigger.setAttribute('aria-expanded', String(open));
407
+ }
408
+
409
+ function closeSessionList() {
410
+ sessionList.classList.remove('open');
411
+ sessionTrigger.setAttribute('aria-expanded', 'false');
412
+ }
413
+
414
+ // ---- Event listeners ----
415
+ themeToggle.addEventListener('click', cycleTheme);
416
+
417
+ sessionTrigger.addEventListener('click', (e) => {
418
+ e.stopPropagation();
419
+ toggleSessionList();
420
+ });
421
+
422
+ document.addEventListener('click', (e) => {
423
+ if (!sessionTrigger.contains(e.target) && !sessionList.contains(e.target)) {
424
+ closeSessionList();
425
+ }
426
+ });
427
+
428
+ window.addEventListener('resize', () => {
429
+ if (prefersReducedMotion()) {
430
+ renderActivityGraph(getSession());
431
+ } else {
432
+ window.requestAnimationFrame(() => renderActivityGraph(getSession()));
433
+ }
434
+ });
435
+
436
+ // Keep duration ticking
437
+ setInterval(() => {
438
+ const session = getSession();
439
+ if (session && session.status === 'running') {
440
+ renderTelemetry(session);
441
+ }
442
+ }, 1000);
443
+
444
+ // Init
445
+ applyTheme(state.theme);
446
+ connect();
447
+ })();
@@ -0,0 +1,96 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>CrewLoop Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <script src="https://unpkg.com/@phosphor-icons/web@2.1.1"></script>
11
+ <link rel="stylesheet" href="styles.css" />
12
+ </head>
13
+ <body>
14
+ <div class="app">
15
+ <header class="header">
16
+ <div class="brand">
17
+ <span class="brand-name">CREWLOOP</span>
18
+ <span class="brand-separator">·</span>
19
+ <span class="brand-label">DASHBOARD</span>
20
+ </div>
21
+ <div class="header-actions">
22
+ <button id="theme-toggle" class="icon-button" aria-label="Toggle theme">
23
+ <i class="ph ph-moon"></i>
24
+ </button>
25
+ <div class="session-selector">
26
+ <button id="session-trigger" class="session-trigger" aria-haspopup="listbox" aria-expanded="false">
27
+ <span class="connection-dot" id="connection-dot"></span>
28
+ <span id="session-label">No session</span>
29
+ <i class="ph ph-caret-down"></i>
30
+ </button>
31
+ <ul id="session-list" class="session-list" role="listbox" aria-label="Sessions"></ul>
32
+ </div>
33
+ </div>
34
+ </header>
35
+
36
+ <main class="main">
37
+ <aside class="sidebar">
38
+ <section class="panel active-skill-panel" aria-live="polite" aria-atomic="true">
39
+ <div class="panel-accent-strip" id="active-strip"></div>
40
+ <div class="active-skill-content">
41
+ <div class="active-skill-icon-wrap">
42
+ <i class="ph ph-circle" id="active-skill-icon"></i>
43
+ </div>
44
+ <h1 class="active-skill-name" id="active-skill-name">IDLE</h1>
45
+ <div class="active-skill-meta">
46
+ <span class="status-badge" id="status-badge">
47
+ <span class="status-dot" id="status-dot"></span>
48
+ <span id="status-text">IDLE</span>
49
+ </span>
50
+ <span class="confidence-badge" id="confidence-badge">unknown</span>
51
+ </div>
52
+ <div class="active-skill-source" id="active-skill-source">
53
+ <i class="ph ph-monitor"></i>
54
+ <span>waiting for events</span>
55
+ </div>
56
+ </div>
57
+ </section>
58
+
59
+ <section class="panel telemetry-panel">
60
+ <h2 class="panel-title">Telemetry</h2>
61
+ <div class="telemetry-grid">
62
+ <div class="telemetry-card">
63
+ <span class="telemetry-value" id="tool-count">0</span>
64
+ <span class="telemetry-label">TOOLS</span>
65
+ </div>
66
+ <div class="telemetry-card">
67
+ <span class="telemetry-value" id="duration">00:00</span>
68
+ <span class="telemetry-label">DURATION</span>
69
+ </div>
70
+ <div class="telemetry-card">
71
+ <span class="telemetry-value" id="event-rate">0</span>
72
+ <span class="telemetry-label">RATE/M</span>
73
+ </div>
74
+ </div>
75
+ </section>
76
+ </aside>
77
+
78
+ <div class="content">
79
+ <section class="panel activity-panel">
80
+ <h2 class="panel-title">Skill Activity</h2>
81
+ <div class="activity-graph" id="activity-graph">
82
+ <div class="activity-empty">Waiting for agent activity...</div>
83
+ </div>
84
+ </section>
85
+
86
+ <section class="panel timeline-panel">
87
+ <h2 class="panel-title">Event Timeline</h2>
88
+ <ul class="timeline" id="timeline" aria-label="Event timeline"></ul>
89
+ </section>
90
+ </div>
91
+ </main>
92
+ </div>
93
+
94
+ <script src="app.js"></script>
95
+ </body>
96
+ </html>