copilot-lens 1.0.0

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/public/app.js ADDED
@@ -0,0 +1,398 @@
1
+ // State
2
+ let sessions = [];
3
+ let analytics = null;
4
+ let charts = {};
5
+
6
+ // Directory color coding — sophisticated muted palette
7
+ const DIR_COLORS = [
8
+ { border: "#6e7681", bg: "#161b2208" }, // slate
9
+ { border: "#58a6ff", bg: "#58a6ff08" }, // blue
10
+ { border: "#3fb950", bg: "#3fb95008" }, // green
11
+ { border: "#d29922", bg: "#d2992208" }, // amber
12
+ { border: "#bc8cff", bg: "#bc8cff08" }, // purple
13
+ { border: "#f0883e", bg: "#f0883e08" }, // orange
14
+ { border: "#56d4dd", bg: "#56d4dd08" }, // teal
15
+ { border: "#db61a2", bg: "#db61a208" }, // rose
16
+ ];
17
+ const dirColorMap = {};
18
+ let nextColorIdx = 0;
19
+
20
+ function getDirColor(dir) {
21
+ if (!dir) return DIR_COLORS[0];
22
+ // Normalize to project root
23
+ const key = dir.replace(/\\/g, "/").split("/").slice(0, -1).join("/") || dir;
24
+ if (!dirColorMap[key]) {
25
+ dirColorMap[key] = DIR_COLORS[nextColorIdx % DIR_COLORS.length];
26
+ nextColorIdx++;
27
+ }
28
+ return dirColorMap[key];
29
+ }
30
+
31
+ // DOM refs
32
+ const sessionList = document.getElementById("sessionList");
33
+ const sessionCount = document.getElementById("sessionCount");
34
+ const searchInput = document.getElementById("searchInput");
35
+ const timeFilter = document.getElementById("timeFilter");
36
+ const statusFilter = document.getElementById("statusFilter");
37
+ const dirFilter = document.getElementById("dirFilter");
38
+ const detailModal = document.getElementById("detailModal");
39
+ const detailContent = document.getElementById("detailContent");
40
+ const modalClose = document.getElementById("modalClose");
41
+ const refreshBtn = document.getElementById("refreshBtn");
42
+
43
+ // Navigation
44
+ document.querySelectorAll(".nav-btn").forEach((btn) => {
45
+ btn.addEventListener("click", () => {
46
+ document.querySelectorAll(".nav-btn").forEach((b) => b.classList.remove("active"));
47
+ document.querySelectorAll(".page").forEach((p) => p.classList.remove("active"));
48
+ btn.classList.add("active");
49
+ document.getElementById(btn.dataset.page + "Page").classList.add("active");
50
+ if (btn.dataset.page === "analytics") loadAnalytics();
51
+ });
52
+ });
53
+
54
+ // Modal
55
+ modalClose.addEventListener("click", () => detailModal.classList.add("hidden"));
56
+ detailModal.addEventListener("click", (e) => {
57
+ if (e.target === detailModal) detailModal.classList.add("hidden");
58
+ });
59
+
60
+ // Format helpers
61
+ function formatDuration(ms) {
62
+ if (ms < 1000) return "< 1s";
63
+ const s = Math.floor(ms / 1000);
64
+ if (s < 60) return s + "s";
65
+ const m = Math.floor(s / 60);
66
+ if (m < 60) return m + "m " + (s % 60) + "s";
67
+ const h = Math.floor(m / 60);
68
+ return h + "h " + (m % 60) + "m";
69
+ }
70
+
71
+ function formatTime(iso) {
72
+ if (!iso) return "—";
73
+ const d = new Date(iso);
74
+ const now = new Date();
75
+ const diff = now - d;
76
+ if (diff < 60000) return "just now";
77
+ if (diff < 3600000) return Math.floor(diff / 60000) + "m ago";
78
+ if (diff < 86400000) return Math.floor(diff / 3600000) + "h ago";
79
+ return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
80
+ }
81
+
82
+ function shortId(id) {
83
+ return id.slice(0, 8);
84
+ }
85
+
86
+ function shortDir(dir) {
87
+ if (!dir) return "—";
88
+ const parts = dir.replace(/\\/g, "/").split("/");
89
+ return parts.slice(-2).join("/");
90
+ }
91
+
92
+ // Filter sessions
93
+ function getFilteredSessions() {
94
+ let filtered = [...sessions];
95
+ const query = searchInput.value.toLowerCase();
96
+ if (query) {
97
+ filtered = filtered.filter(
98
+ (s) =>
99
+ s.id.toLowerCase().includes(query) ||
100
+ (s.cwd || "").toLowerCase().includes(query) ||
101
+ (s.branch || "").toLowerCase().includes(query)
102
+ );
103
+ }
104
+
105
+ const time = timeFilter.value;
106
+ if (time !== "all") {
107
+ const now = new Date();
108
+ filtered = filtered.filter((s) => {
109
+ const created = new Date(s.createdAt);
110
+ if (time === "today") return now - created < 86400000;
111
+ if (time === "week") return now - created < 604800000;
112
+ if (time === "month") return now - created < 2592000000;
113
+ return true;
114
+ });
115
+ }
116
+
117
+ const status = statusFilter.value;
118
+ if (status !== "all") {
119
+ filtered = filtered.filter((s) => s.status === status);
120
+ }
121
+
122
+ const dir = dirFilter.value;
123
+ if (dir !== "all") {
124
+ filtered = filtered.filter((s) => (s.cwd || "") === dir);
125
+ }
126
+
127
+ return filtered;
128
+ }
129
+
130
+ // Render session list
131
+ function renderSessions() {
132
+ const filtered = getFilteredSessions();
133
+ sessionCount.textContent = `${filtered.length} session${filtered.length !== 1 ? "s" : ""} found`;
134
+
135
+ sessionList.innerHTML = filtered
136
+ .map(
137
+ (s) => {
138
+ const c = getDirColor(s.cwd);
139
+ return `
140
+ <div class="session-card" data-id="${s.id}" style="border-left: 3px solid ${c.border}">
141
+ <div class="top-row">
142
+ <span class="session-id">${shortId(s.id)}</span>
143
+ <span class="badge badge-${s.status}">${s.status === "running" ? "● Running" : s.status === "error" ? "✕ Error" : "✓ Completed"}</span>
144
+ </div>
145
+ <div class="session-dir">${s.cwd || "—"}</div>
146
+ <div class="session-meta">
147
+ ${s.branch ? `<span class="badge badge-branch">⎇ ${s.branch}</span>` : ""}
148
+ <span>${formatTime(s.createdAt)}</span>
149
+ </div>
150
+ </div>
151
+ `;
152
+ }
153
+ )
154
+ .join("");
155
+
156
+ // Click handlers
157
+ sessionList.querySelectorAll(".session-card").forEach((card) => {
158
+ card.addEventListener("click", () => openDetail(card.dataset.id));
159
+ });
160
+ }
161
+
162
+ // Open session detail
163
+ async function openDetail(id) {
164
+ detailContent.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-dim)">Loading...</div>';
165
+ detailModal.classList.remove("hidden");
166
+
167
+ try {
168
+ const res = await fetch(`/api/sessions/${id}`);
169
+ const session = await res.json();
170
+ renderDetail(session);
171
+ } catch (err) {
172
+ detailContent.innerHTML = `<div style="color:var(--danger)">Failed to load session: ${err.message}</div>`;
173
+ }
174
+ }
175
+
176
+ function renderDetail(s) {
177
+ const userMessages = s.events.filter((e) => e.type === "user.message");
178
+ const assistantMessages = s.events.filter((e) => e.type === "assistant.message");
179
+ const toolCalls = s.events.filter((e) => e.type === "tool.execution_start");
180
+ const errors = s.events.filter((e) => e.type === "session.error");
181
+
182
+ // Interleave conversation messages in order
183
+ const conversation = s.events
184
+ .filter((e) => e.type === "user.message" || e.type === "assistant.message")
185
+ .map((e) => {
186
+ const isUser = e.type === "user.message";
187
+ const content = e.data?.content || "";
188
+ // Truncate long messages
189
+ const display = content.length > 2000 ? content.slice(0, 2000) + "\n...(truncated)" : content;
190
+ return `<div class="message ${isUser ? "message-user" : "message-assistant"}">
191
+ <div class="message-label">${isUser ? "👤 You" : "🤖 Copilot"}</div>
192
+ ${escapeHtml(display)}
193
+ </div>`;
194
+ })
195
+ .join("");
196
+
197
+ detailContent.innerHTML = `
198
+ <div class="detail-header">
199
+ <h2>Session ${s.id}</h2>
200
+ <div class="detail-meta">
201
+ <div><span>Directory:</span> <strong>${s.cwd || "—"}</strong></div>
202
+ <div><span>Branch:</span> <strong>${s.branch || "—"}</strong></div>
203
+ <div><span>Created:</span> <strong>${new Date(s.createdAt).toLocaleString()}</strong></div>
204
+ <div><span>Duration:</span> <strong>${formatDuration(s.duration)}</strong></div>
205
+ <div><span>Version:</span> <strong>${s.copilotVersion || "—"}</strong></div>
206
+ <div><span>Status:</span> <strong class="badge badge-${s.status}">${s.status === "running" ? "● Running" : s.status === "error" ? "✕ Error" : "✓ Completed"}</strong></div>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="event-counts" style="margin-bottom:16px">
211
+ ${Object.entries(s.eventCounts)
212
+ .sort((a, b) => b[1] - a[1])
213
+ .map(([type, count]) => `<span class="event-count-badge">${type}: ${count}</span>`)
214
+ .join("")}
215
+ </div>
216
+
217
+ <div class="detail-tabs">
218
+ <button class="detail-tab active" data-tab="conversation">Conversation (${userMessages.length + assistantMessages.length})</button>
219
+ <button class="detail-tab" data-tab="tools">Tools (${toolCalls.length})</button>
220
+ <button class="detail-tab" data-tab="errors">Errors (${errors.length})</button>
221
+ ${s.planContent ? '<button class="detail-tab" data-tab="plan">Plan</button>' : ""}
222
+ </div>
223
+
224
+ <div class="detail-panel active" id="panel-conversation">
225
+ ${conversation || '<div style="color:var(--text-dim)">No messages in this session</div>'}
226
+ </div>
227
+
228
+ <div class="detail-panel" id="panel-tools">
229
+ ${
230
+ toolCalls.length
231
+ ? toolCalls
232
+ .map((e) => `<div class="tool-item">${e.data?.tool || e.data?.toolName || "unknown"}</div>`)
233
+ .join("")
234
+ : '<div style="color:var(--text-dim)">No tool calls</div>'
235
+ }
236
+ </div>
237
+
238
+ <div class="detail-panel" id="panel-errors">
239
+ ${
240
+ errors.length
241
+ ? errors
242
+ .map(
243
+ (e) =>
244
+ `<div class="message" style="border-left:3px solid var(--danger)"><div class="message-label" style="color:var(--danger)">Error</div>${escapeHtml(e.data?.message || "Unknown error")}</div>`
245
+ )
246
+ .join("")
247
+ : '<div style="color:var(--text-dim)">No errors 🎉</div>'
248
+ }
249
+ </div>
250
+
251
+ ${s.planContent ? `<div class="detail-panel" id="panel-plan"><div class="plan-content">${escapeHtml(s.planContent)}</div></div>` : ""}
252
+ `;
253
+
254
+ // Tab switching
255
+ detailContent.querySelectorAll(".detail-tab").forEach((tab) => {
256
+ tab.addEventListener("click", () => {
257
+ detailContent.querySelectorAll(".detail-tab").forEach((t) => t.classList.remove("active"));
258
+ detailContent.querySelectorAll(".detail-panel").forEach((p) => p.classList.remove("active"));
259
+ tab.classList.add("active");
260
+ document.getElementById("panel-" + tab.dataset.tab).classList.add("active");
261
+ });
262
+ });
263
+ }
264
+
265
+ function escapeHtml(str) {
266
+ const div = document.createElement("div");
267
+ div.textContent = str;
268
+ return div.innerHTML;
269
+ }
270
+
271
+ // Analytics
272
+ async function loadAnalytics() {
273
+ try {
274
+ const res = await fetch("/api/analytics");
275
+ analytics = await res.json();
276
+ renderAnalytics();
277
+ } catch (err) {
278
+ statsCards.innerHTML = `<div style="color:var(--danger)">Failed to load analytics</div>`;
279
+ }
280
+ }
281
+
282
+ function renderAnalytics() {
283
+ if (!analytics) return;
284
+
285
+ // Stats cards
286
+ statsCards.innerHTML = `
287
+ <div class="stat-card"><div class="stat-value">${analytics.totalSessions}</div><div class="stat-label">Total Sessions</div></div>
288
+ <div class="stat-card"><div class="stat-value">${formatDuration(analytics.avgDuration)}</div><div class="stat-label">Avg Duration</div></div>
289
+ <div class="stat-card"><div class="stat-value">${formatDuration(analytics.maxDuration)}</div><div class="stat-label">Longest Session</div></div>
290
+ <div class="stat-card"><div class="stat-value">${formatDuration(analytics.totalDuration)}</div><div class="stat-label">Total Time</div></div>
291
+ `;
292
+
293
+ renderCharts();
294
+ }
295
+
296
+ function renderCharts() {
297
+ // Destroy existing charts
298
+ Object.values(charts).forEach((c) => c.destroy());
299
+ charts = {};
300
+
301
+ const chartColors = ["#58a6ff", "#3fb950", "#d29922", "#f85149", "#bc8cff", "#f0883e", "#56d4dd", "#db61a2"];
302
+
303
+ // Sessions per day
304
+ const days = Object.keys(analytics.sessionsPerDay).sort();
305
+ charts.perDay = new Chart(document.getElementById("sessionsPerDayChart"), {
306
+ type: "bar",
307
+ data: {
308
+ labels: days.map((d) => d.slice(5)), // MM-DD
309
+ datasets: [{ label: "Sessions", data: days.map((d) => analytics.sessionsPerDay[d]), backgroundColor: "#58a6ff88", borderColor: "#58a6ff", borderWidth: 1 }],
310
+ },
311
+ options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: "#8b949e" } }, x: { ticks: { color: "#8b949e" } } } },
312
+ });
313
+
314
+ // Tool usage (top 10)
315
+ const tools = Object.entries(analytics.toolUsage)
316
+ .sort((a, b) => b[1] - a[1])
317
+ .slice(0, 10);
318
+ if (tools.length) {
319
+ charts.tools = new Chart(document.getElementById("toolUsageChart"), {
320
+ type: "doughnut",
321
+ data: {
322
+ labels: tools.map((t) => t[0]),
323
+ datasets: [{ data: tools.map((t) => t[1]), backgroundColor: chartColors }],
324
+ },
325
+ options: { responsive: true, plugins: { legend: { position: "right", labels: { color: "#e6edf3", font: { size: 11 } } } } },
326
+ });
327
+ }
328
+
329
+ // Top directories (top 8)
330
+ const dirs = Object.entries(analytics.topDirectories)
331
+ .sort((a, b) => b[1] - a[1])
332
+ .slice(0, 8);
333
+ if (dirs.length) {
334
+ charts.dirs = new Chart(document.getElementById("topDirsChart"), {
335
+ type: "bar",
336
+ data: {
337
+ labels: dirs.map((d) => shortDir(d[0])),
338
+ datasets: [{ label: "Sessions", data: dirs.map((d) => d[1]), backgroundColor: "#3fb95088", borderColor: "#3fb950", borderWidth: 1 }],
339
+ },
340
+ options: { indexAxis: "y", responsive: true, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { color: "#8b949e" } }, y: { ticks: { color: "#8b949e", font: { size: 11 } } } } },
341
+ });
342
+ }
343
+
344
+ // Branch activity (top 8)
345
+ const branches = Object.entries(analytics.branchActivity)
346
+ .sort((a, b) => b[1] - a[1])
347
+ .slice(0, 8);
348
+ if (branches.length) {
349
+ charts.branches = new Chart(document.getElementById("branchChart"), {
350
+ type: "bar",
351
+ data: {
352
+ labels: branches.map((b) => b[0]),
353
+ datasets: [{ label: "Sessions", data: branches.map((b) => b[1]), backgroundColor: "#d2992288", borderColor: "#d29922", borderWidth: 1 }],
354
+ },
355
+ options: { indexAxis: "y", responsive: true, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { color: "#8b949e" } }, y: { ticks: { color: "#8b949e", font: { size: 11 } } } } },
356
+ });
357
+ }
358
+ }
359
+
360
+ // Data loading
361
+ async function loadSessions() {
362
+ try {
363
+ const res = await fetch("/api/sessions");
364
+ sessions = await res.json();
365
+ updateDirFilter();
366
+ renderSessions();
367
+ } catch (err) {
368
+ sessionList.innerHTML = `<div style="color:var(--danger);padding:20px">Failed to load sessions: ${err.message}</div>`;
369
+ }
370
+ }
371
+
372
+ // Event listeners
373
+ searchInput.addEventListener("input", renderSessions);
374
+ timeFilter.addEventListener("change", renderSessions);
375
+ statusFilter.addEventListener("change", renderSessions);
376
+ dirFilter.addEventListener("change", renderSessions);
377
+
378
+ // Populate directory filter from session data
379
+ function updateDirFilter() {
380
+ const dirs = [...new Set(sessions.map((s) => s.cwd || "").filter(Boolean))].sort();
381
+ const current = dirFilter.value;
382
+ dirFilter.innerHTML = '<option value="all">All Directories</option>' +
383
+ dirs.map((d) => `<option value="${d}">${shortDir(d)}</option>`).join("");
384
+ dirFilter.value = current;
385
+ }
386
+
387
+ // Refresh button
388
+ refreshBtn.addEventListener("click", () => {
389
+ refreshBtn.classList.add("spinning");
390
+ loadSessions();
391
+ if (document.getElementById("analyticsPage").classList.contains("active")) {
392
+ loadAnalytics();
393
+ }
394
+ setTimeout(() => refreshBtn.classList.remove("spinning"), 600);
395
+ });
396
+
397
+ // Init
398
+ loadSessions();
@@ -0,0 +1,79 @@
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>Copilot Lens</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <div class="logo">👓 Copilot Lens</div>
13
+ <nav>
14
+ <button class="nav-btn active" data-page="sessions">Sessions</button>
15
+ <button class="nav-btn" data-page="analytics">Analytics</button>
16
+ </nav>
17
+ <button class="refresh-btn" id="refreshBtn" title="Refresh data">⟳ Refresh</button>
18
+ </header>
19
+
20
+ <main>
21
+ <!-- Sessions Page -->
22
+ <section id="sessionsPage" class="page active">
23
+ <div class="controls">
24
+ <input type="text" id="searchInput" placeholder="Search sessions (ID, branch)..." />
25
+ <select id="timeFilter">
26
+ <option value="all">All Time</option>
27
+ <option value="today">Today</option>
28
+ <option value="week">This Week</option>
29
+ <option value="month">This Month</option>
30
+ </select>
31
+ <select id="statusFilter">
32
+ <option value="all">All Status</option>
33
+ <option value="running">Running</option>
34
+ <option value="completed">Completed</option>
35
+ <option value="error">Error</option>
36
+ </select>
37
+ <select id="dirFilter">
38
+ <option value="all">All Directories</option>
39
+ </select>
40
+ </div>
41
+ <div id="sessionCount" class="session-count"></div>
42
+ <div id="sessionList" class="session-list"></div>
43
+ </section>
44
+
45
+ <!-- Analytics Page -->
46
+ <section id="analyticsPage" class="page">
47
+ <div class="stats-cards" id="statsCards"></div>
48
+ <div class="charts-grid">
49
+ <div class="chart-card">
50
+ <h3>Sessions Per Day</h3>
51
+ <canvas id="sessionsPerDayChart"></canvas>
52
+ </div>
53
+ <div class="chart-card">
54
+ <h3>Tool Usage</h3>
55
+ <canvas id="toolUsageChart"></canvas>
56
+ </div>
57
+ <div class="chart-card">
58
+ <h3>Top Working Directories</h3>
59
+ <canvas id="topDirsChart"></canvas>
60
+ </div>
61
+ <div class="chart-card">
62
+ <h3>Branch Activity</h3>
63
+ <canvas id="branchChart"></canvas>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ <!-- Session Detail Modal -->
69
+ <div id="detailModal" class="modal hidden">
70
+ <div class="modal-content">
71
+ <button class="modal-close" id="modalClose">✕</button>
72
+ <div id="detailContent"></div>
73
+ </div>
74
+ </div>
75
+ </main>
76
+
77
+ <script src="app.js"></script>
78
+ </body>
79
+ </html>