copilot-tracer 1.0.4 → 1.0.6

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/dist/webServer.js CHANGED
@@ -4,11 +4,11 @@ import { Server } from 'socket.io';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execSync } from 'child_process';
7
- import { getTraces, getTrace, getSessionSummary } from './db.js';
7
+ import { getTraces, getTrace, getSessionSummary, getDashboard, updateProjectLocalPath, getProjectTraces, getProjectSessionSummary } from './db.js';
8
8
  import { traceEvents } from './proxy.js';
9
9
  import { registerOtlpRoutes } from './otlpReceiver.js';
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- export function startWebServer(port = 4747, sessionId) {
11
+ export function startWebServer(port = 4747, sessionId, projectId) {
12
12
  const app = express();
13
13
  const httpServer = createServer(app);
14
14
  const io = new Server(httpServer, { cors: { origin: '*' } });
@@ -16,7 +16,7 @@ export function startWebServer(port = 4747, sessionId) {
16
16
  app.use(express.static(path.join(__dirname, '../web')));
17
17
  // Register OTLP receiver routes
18
18
  app.use(express.json({ limit: '10mb' }));
19
- registerOtlpRoutes(app, sessionId ?? 'default');
19
+ registerOtlpRoutes(app, sessionId ?? 'default', projectId);
20
20
  // API
21
21
  app.get('/api/traces', (req, res) => {
22
22
  const sid = req.query.sessionId || undefined; // undefined = all sessions
@@ -81,14 +81,45 @@ ${prompt.trim()}`;
81
81
  return res.json(null);
82
82
  res.json(getSessionSummary(sid));
83
83
  });
84
+ app.get('/api/dashboard', (_req, res) => {
85
+ res.json(getDashboard());
86
+ });
87
+ // Project registration — CLI registers its local path for a project
88
+ app.post('/api/projects', (req, res) => {
89
+ const { projectId, localPath } = req.body;
90
+ if (!projectId || !localPath) {
91
+ res.status(400).json({ error: 'projectId and localPath required' });
92
+ return;
93
+ }
94
+ updateProjectLocalPath(projectId, localPath);
95
+ res.json({ ok: true });
96
+ });
97
+ // Project-scoped traces
98
+ app.get('/api/projects/:id/traces', (req, res) => {
99
+ const traces = getProjectTraces(req.params.id, 200);
100
+ res.json(traces);
101
+ });
102
+ // Project-scoped summary
103
+ app.get('/api/projects/:id/summary', (req, res) => {
104
+ res.json(getProjectSessionSummary(req.params.id));
105
+ });
84
106
  // Socket.io — push real-time updates
85
107
  io.on('connection', (socket) => {
108
+ const projectId = socket.handshake.query.projectId;
86
109
  // Send current state on connect
87
- const sid = sessionId;
88
- socket.emit('init', {
89
- traces: getTraces(sid, 200),
90
- summary: sid ? getSessionSummary(sid) : null,
91
- });
110
+ if (projectId) {
111
+ socket.emit('init', {
112
+ traces: getProjectTraces(projectId, 200),
113
+ summary: getProjectSessionSummary(projectId),
114
+ });
115
+ }
116
+ else {
117
+ const sid = sessionId;
118
+ socket.emit('init', {
119
+ traces: getTraces(sid, 200),
120
+ summary: sid ? getSessionSummary(sid) : null,
121
+ });
122
+ }
92
123
  const onUpdate = (entry) => socket.emit('trace:update', entry);
93
124
  const onDone = (entry) => socket.emit('trace:done', entry);
94
125
  traceEvents.on('trace:update', onUpdate);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tracer",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Real-time tracing and prompt-refinement companion for GitHub Copilot CLI and VS Code Copilot — tracks tokens, AI credits, tool calls, and refined prompts with console and web UI",
5
5
  "keywords": [
6
6
  "copilot",
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/chuongnd/copilot-tracer.git"
17
+ "url": "https://github.com/chuongxl/copilot-tracer.git"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=18"
package/web/index.html CHANGED
@@ -8,13 +8,11 @@
8
8
  <script type="module">
9
9
  import * as webllm from 'https://esm.run/@mlc-ai/web-llm';
10
10
 
11
- // ─── REFINE ENGINE — Phase 1: instant heuristics · Phase 2: webLLM ───────
12
11
  let refineOpen = false;
13
12
  let engine = null;
14
13
 
15
14
  const tok = t => Math.ceil((t || '').length / 4);
16
15
 
17
- // Phase 1 — instant local analysis
18
16
  function analyzePrompt(t) {
19
17
  const issues = [], suggestions = [];
20
18
  if ([/\bplease\b/,/\bcould you\b/,/\bcan you\b/,/\bI want you to\b/,/\bkindly\b/,/\bthank you\b/,/\bif you don.t mind\b/].some(r => r.test(t)))
@@ -66,10 +64,9 @@
66
64
  <div class=refine-optimized id=refined-text style="display:none"></div>
67
65
  <div id=refine-actions style="display:none">
68
66
  <button class="refine-copy-btn" onclick="copyRefined()">📋 Copy optimized prompt</button>
69
- <div class=refine-stats id=refine-stats></div>
67
+ <div class="refine-stats" id=refine-stats></div>
70
68
  </div>`;
71
69
 
72
- // Phase 2 — webLLM
73
70
  const progressEl = () => document.getElementById('refine-progress');
74
71
  const optimizingEl = document.getElementById('refine-optimizing');
75
72
  const refinedEl = document.getElementById('refined-text');
@@ -127,8 +124,23 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
127
124
  </script>
128
125
  <style>
129
126
  * { box-sizing: border-box; margin: 0; padding: 0; }
130
- body { background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', system-ui, monospace; font-size: 13px; }
131
-
127
+ body { background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', system-ui, monospace; font-size: 13px; display: flex; height: 100vh; }
128
+
129
+ /* ── Sidebar ─────────────────────────────────── */
130
+ .sidebar { width: 56px; background: #161b22; border-right: 1px solid #30363d; display: flex; flex-direction: column; align-items: center; padding: 8px 0; gap: 4px; flex-shrink: 0; }
131
+ .sidebar-brand { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-bottom: 12px; }
132
+ .sidebar-link { width: 40px; height: 40px; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 8px; text-decoration: none; color: #8b949e; transition: all 0.15s; cursor: pointer; gap: 2px; }
133
+ .sidebar-link:hover { background: #21262d; color: #c9d1d9; }
134
+ .sidebar-link.active { background: #1f3358; color: #58a6ff; }
135
+ .sidebar-icon { font-size: 16px; line-height: 1; }
136
+ .sidebar-label { font-size: 8px; text-transform: uppercase; letter-spacing: 0.3px; }
137
+
138
+ /* ── Main content area ───────────────────────── */
139
+ .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
140
+ .page { display: none; flex-direction: column; flex: 1; overflow: hidden; }
141
+ .page.active { display: flex; }
142
+
143
+ /* ── Header (live tracer) ────────────────────── */
132
144
  header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
133
145
  header h1 { color: #58a6ff; font-size: 16px; font-weight: 700; }
134
146
  .badge { background: #21262d; border: 1px solid #30363d; border-radius: 12px; padding: 2px 10px; font-size: 11px; color: #8b949e; }
@@ -136,7 +148,8 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
136
148
  #status-dot { width: 8px; height: 8px; border-radius: 50%; background: #3fb950; display: inline-block; animation: pulse 2s infinite; }
137
149
  @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
138
150
 
139
- .stats-bar { background: #161b22; border-bottom: 1px solid #30363d; padding: 8px 20px; display: flex; gap: 24px; }
151
+ /* ── Stats bar (live tracer) ─────────────────── */
152
+ .stats-bar { background: #161b22; border-bottom: 1px solid #30363d; padding: 8px 20px; display: flex; gap: 24px; flex-wrap: wrap; }
140
153
  .stat { display: flex; flex-direction: column; }
141
154
  .stat-label { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
142
155
  .stat-value { font-size: 15px; font-weight: 700; color: #e6edf3; }
@@ -145,9 +158,10 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
145
158
  .stat-value.skills { color: #d2a8ff; }
146
159
  .stat-value.mcps { color: #56d364; }
147
160
 
148
- .main { display: flex; height: calc(100vh - 100px); }
161
+ /* ── Main split (live tracer) ────────────────── */
162
+ .main { display: flex; flex: 1; overflow: hidden; }
149
163
  .table-panel { flex: 1; overflow: auto; }
150
- .detail-panel { width: 420px; border-left: 1px solid #30363d; background: #161b22; overflow: auto; display: none; flex-direction: column; }
164
+ .detail-panel { width: 420px; border-left: 1px solid #30363d; background: #161b22; overflow: auto; display: none; flex-direction: column; flex-shrink: 0; }
151
165
  .detail-panel.open { display: flex; }
152
166
 
153
167
  table { width: 100%; border-collapse: collapse; }
@@ -177,7 +191,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
177
191
  .pill-mcp { background: #0a3622; color: #56d364; }
178
192
  .pill:hover { opacity: 0.8; filter: brightness(1.2); }
179
193
 
180
- /* Detail panel */
194
+ /* ── Detail panel ────────────────────────────── */
181
195
  .detail-header { padding: 14px 16px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; }
182
196
  .detail-header h3 { color: #58a6ff; font-size: 13px; }
183
197
  .close-btn { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 18px; line-height: 1; }
@@ -190,7 +204,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
190
204
  .detail-box.response { color: #56d364; }
191
205
  .detail-box.prompt { color: #e6edf3; max-height: none; overflow: visible; }
192
206
 
193
- /* Call graph */
207
+ /* ── Call graph ──────────────────────────────── */
194
208
  .call-tree { font-family: monospace; font-size: 12px; }
195
209
  .call-node { padding: 4px 0; }
196
210
  .call-node-header { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 4px 6px; border-radius: 4px; }
@@ -203,7 +217,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
203
217
  .call-io .label { color: #58a6ff; margin-right: 4px; }
204
218
  .call-io.error { color: #f85149; }
205
219
 
206
- /* Refine panel */
220
+ /* ── Refine panel ────────────────────────────── */
207
221
  .refine-btn { background: #1f2d1f; border: 1px solid #238636; color: #7ee787; border-radius: 6px; padding: 5px 12px; font-size: 11px; cursor: pointer; margin-top: 8px; display: inline-flex; align-items: center; gap: 6px; transition: all .15s; }
208
222
  .refine-btn:hover { background: #238636; color: #fff; }
209
223
  .refine-panel { margin-top: 12px; background: #0d1117; border: 1px solid #238636; border-radius: 8px; padding: 14px; display: none; }
@@ -227,6 +241,8 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
227
241
  .refine-suggestion { font-size: 11px; padding: 4px 8px; border-radius: 4px; background: #0d2230; border-left: 3px solid #1f6feb; color: #8b949e; }
228
242
  @keyframes refine-spin { to { transform: rotate(360deg); } }
229
243
  .refine-spinner { display: inline-block; width: 10px; height: 10px; border: 2px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: refine-spin .7s linear infinite; vertical-align: middle; margin-right: 4px; }
244
+
245
+ /* ── Modal ───────────────────────────────────── */
230
246
  .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; }
231
247
  .modal-overlay.open { display: flex; }
232
248
  .modal { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; width: 560px; max-height: 80vh; overflow: auto; }
@@ -234,58 +250,118 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
234
250
  .modal-close { float: right; background: none; border: none; color: #8b949e; cursor: pointer; font-size: 20px; margin-top: -4px; }
235
251
 
236
252
  .empty { padding: 40px; text-align: center; color: #8b949e; }
253
+
254
+ /* ── Dashboard page ──────────────────────────── */
255
+ .dash-header { padding: 20px 24px 0; }
256
+ .dash-header h2 { color: #e6edf3; font-size: 18px; font-weight: 600; }
257
+ .dash-header p { color: #8b949e; font-size: 12px; margin-top: 4px; }
258
+
259
+ .dash-totals { display: flex; gap: 16px; padding: 16px 24px; flex-wrap: wrap; }
260
+ .dash-total-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px 20px; min-width: 140px; flex: 1; }
261
+ .dash-total-label { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
262
+ .dash-total-value { font-size: 22px; font-weight: 700; margin-top: 4px; }
263
+ .dash-total-value.credits { color: #f0883e; }
264
+ .dash-total-value.tokens { color: #79c0ff; }
265
+ .dash-total-value.sessions { color: #d2a8ff; }
266
+
267
+ .dash-section-title { padding: 16px 24px 8px; font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
268
+
269
+ .dash-projects { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; padding: 0 24px 24px; }
270
+ .dash-project-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; cursor: pointer; transition: border-color 0.15s; }
271
+ .dash-project-card:hover { border-color: #58a6ff; }
272
+ .dash-project-path { color: #e6edf3; font-size: 13px; font-weight: 600; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
273
+ .dash-project-stats { display: flex; gap: 16px; flex-wrap: wrap; }
274
+ .dash-project-stat { display: flex; flex-direction: column; }
275
+ .dash-project-stat-label { font-size: 9px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
276
+ .dash-project-stat-value { font-size: 14px; font-weight: 600; color: #c9d1d9; margin-top: 2px; }
277
+ .dash-project-stat-value.credits { color: #f0883e; }
278
+ .dash-project-last-active { margin-top: 10px; font-size: 11px; color: #8b949e; }
279
+ .dash-empty { padding: 60px 24px; text-align: center; color: #8b949e; }
280
+ .dash-empty p { margin-top: 8px; font-size: 12px; }
281
+ .dash-empty code { background: #21262d; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #79c0ff; }
237
282
  </style>
238
283
  </head>
239
284
  <body>
240
285
 
241
- <header>
242
- <span id="status-dot"></span>
243
- <h1>🤖 Copilot Tracer</h1>
244
- <span class="badge live">LIVE</span>
245
- <span class="badge" id="session-badge">Session: —</span>
246
- </header>
247
-
248
- <div class="stats-bar">
249
- <div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
250
- <div class="stat"><span class="stat-label">AI Credits</span><span class="stat-value credits" id="s-credits">0.00 cr | $0.0000</span></div>
251
- <div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
252
- <div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
253
- <div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
254
- <div class="stat"><span class="stat-label">Skill Calls</span><span class="stat-value skills" id="s-skills">0</span></div>
255
- <div class="stat"><span class="stat-label">Agent Calls</span><span class="stat-value tokens" id="s-agents">0</span></div>
256
- <div class="stat"><span class="stat-label">MCP Calls</span><span class="stat-value mcps" id="s-mcps">0</span></div>
257
- <div class="stat"><span class="stat-label">Total Duration</span><span class="stat-value" id="s-duration">0s</span></div>
258
- </div>
259
-
260
- <div class="main">
261
- <div class="table-panel">
262
- <table>
263
- <thead>
264
- <tr>
265
- <th>Date / Time</th>
266
- <th>Prompt</th>
267
- <th>AI Credits</th>
268
- <th>Duration</th>
269
- <th>Cached Tokens</th>
270
- <th>Written Tokens</th>
271
- <th>Reasoning Tokens</th>
272
- <th>Skills</th>
273
- <th>Agents</th>
274
- <th>MCPs</th>
275
- </tr>
276
- </thead>
277
- <tbody id="trace-tbody">
278
- <tr><td colspan="10" class="empty">Waiting for Copilot CLI activity...</td></tr>
279
- </tbody>
280
- </table>
286
+ <!-- ── Sidebar ──────────────────────────────── -->
287
+ <nav class="sidebar">
288
+ <div class="sidebar-brand">🤖</div>
289
+ <a href="#/dashboard" class="sidebar-link active" data-page="dashboard" title="Dashboard">
290
+ <span class="sidebar-icon">📊</span>
291
+ <span class="sidebar-label">Home</span>
292
+ </a>
293
+ <a href="#/live" class="sidebar-link" data-page="live" title="Live Tracer">
294
+ <span class="sidebar-icon">📡</span>
295
+ <span class="sidebar-label">Live</span>
296
+ </a>
297
+ </nav>
298
+
299
+ <!-- ── Main content ─────────────────────────── -->
300
+ <div class="content">
301
+
302
+ <!-- ═══════════════ DASHBOARD PAGE ═══════════════ -->
303
+ <div id="page-dashboard" class="page active">
304
+ <div class="dash-header">
305
+ <h2>Dashboard</h2>
306
+ <p>All traced projects and sessions</p>
307
+ </div>
308
+ <div class="dash-totals" id="dash-totals"></div>
309
+ <div class="dash-section-title">Projects</div>
310
+ <div id="dash-projects" class="dash-projects"></div>
281
311
  </div>
282
312
 
283
- <div class="detail-panel" id="detail-panel">
284
- <div class="detail-header">
285
- <h3 id="detail-title">Trace Detail</h3>
286
- <button class="close-btn" onclick="closeDetail()">×</button>
313
+ <!-- ═══════════════ LIVE TRACER PAGE ═══════════════ -->
314
+ <div id="page-live" class="page">
315
+ <header>
316
+ <span id="status-dot"></span>
317
+ <h1>🤖 Copilot Tracer</h1>
318
+ <span class="badge live">LIVE</span>
319
+ <span class="badge" id="session-badge">Session: —</span>
320
+ </header>
321
+
322
+ <div class="stats-bar">
323
+ <div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
324
+ <div class="stat"><span class="stat-label">AI Credits</span><span class="stat-value credits" id="s-credits">0.00 cr | $0.0000</span></div>
325
+ <div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
326
+ <div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
327
+ <div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
328
+ <div class="stat"><span class="stat-label">Skill Calls</span><span class="stat-value skills" id="s-skills">0</span></div>
329
+ <div class="stat"><span class="stat-label">Agent Calls</span><span class="stat-value tokens" id="s-agents">0</span></div>
330
+ <div class="stat"><span class="stat-label">MCP Calls</span><span class="stat-value mcps" id="s-mcps">0</span></div>
331
+ <div class="stat"><span class="stat-label">Total Duration</span><span class="stat-value" id="s-duration">0s</span></div>
332
+ </div>
333
+
334
+ <div class="main">
335
+ <div class="table-panel">
336
+ <table>
337
+ <thead>
338
+ <tr>
339
+ <th>Date / Time</th>
340
+ <th>Prompt</th>
341
+ <th>AI Credits</th>
342
+ <th>Duration</th>
343
+ <th>Cached Tokens</th>
344
+ <th>Written Tokens</th>
345
+ <th>Reasoning Tokens</th>
346
+ <th>Skills</th>
347
+ <th>Agents</th>
348
+ <th>MCPs</th>
349
+ </tr>
350
+ </thead>
351
+ <tbody id="trace-tbody">
352
+ <tr><td colspan="10" class="empty">Waiting for Copilot CLI activity...</td></tr>
353
+ </tbody>
354
+ </table>
355
+ </div>
356
+
357
+ <div class="detail-panel" id="detail-panel">
358
+ <div class="detail-header">
359
+ <h3 id="detail-title">Trace Detail</h3>
360
+ <button class="close-btn" onclick="closeDetail()">×</button>
361
+ </div>
362
+ <div class="detail-body" id="detail-body"></div>
363
+ </div>
287
364
  </div>
288
- <div class="detail-body" id="detail-body"></div>
289
365
  </div>
290
366
  </div>
291
367
 
@@ -299,30 +375,168 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
299
375
  </div>
300
376
 
301
377
  <script>
378
+ // ── Routing ────────────────────────────────────
379
+ const pages = ['dashboard', 'live'];
302
380
  const socket = io();
303
381
  let traces = {};
304
382
  let summary = null;
305
383
  let selectedId = null;
384
+ let currentProjectId = null;
385
+ let liveSocket = null;
386
+ let dashboardRefreshPending = false;
387
+
388
+ // Main socket listens for all trace events — refreshes dashboard in real-time
389
+ // (regardless of active page, so it's fresh when you switch back)
390
+ socket.on('trace:update', () => scheduleDashboardRefresh());
391
+ socket.on('trace:done', () => scheduleDashboardRefresh());
392
+
393
+ function navigateTo(page, projectId) {
394
+ if (!pages.includes(page)) page = 'dashboard';
395
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
396
+ document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
397
+ const pageEl = document.getElementById('page-' + page);
398
+ const linkEl = document.querySelector(`[data-page="${page}"]`);
399
+ if (pageEl) pageEl.classList.add('active');
400
+ if (linkEl) linkEl.classList.add('active');
401
+
402
+ if (page === 'dashboard') {
403
+ loadDashboard();
404
+ } else if (page === 'live') {
405
+ connectLiveSocket(projectId);
406
+ }
407
+ }
306
408
 
307
- socket.on('init', (data) => {
308
- data.traces.forEach(t => traces[t.id] = t);
309
- summary = data.summary;
310
- render();
311
- });
409
+ function getHashPage() {
410
+ const hash = location.hash.replace('#/', '').split('?')[0];
411
+ return pages.includes(hash) ? hash : 'dashboard';
412
+ }
312
413
 
313
- socket.on('trace:update', (t) => {
314
- traces[t.id] = t;
315
- updateSummaryFromTraces();
316
- render();
317
- if (selectedId === t.id) renderDetail(t);
318
- });
414
+ function getHashProject() {
415
+ const match = location.hash.match(/project=([^&]+)/);
416
+ return match ? decodeURIComponent(match[1]) : null;
417
+ }
319
418
 
320
- socket.on('trace:done', (t) => {
321
- traces[t.id] = t;
322
- updateSummaryFromTraces();
323
- render();
419
+ function connectLiveSocket(projectId) {
420
+ // Disconnect existing live socket
421
+ if (liveSocket) { liveSocket.disconnect(); liveSocket = null; }
422
+ currentProjectId = projectId || null;
423
+
424
+ // Reset state
425
+ traces = {};
426
+ summary = null;
427
+ selectedId = null;
428
+
429
+ // Connect with project filter
430
+ const query = currentProjectId ? { projectId: currentProjectId } : {};
431
+ liveSocket = io({ query });
432
+ liveSocket.on('init', (data) => {
433
+ data.traces.forEach(t => traces[t.id] = t);
434
+ summary = data.summary;
435
+ render();
436
+ });
437
+ liveSocket.on('trace:update', (t) => {
438
+ traces[t.id] = t;
439
+ updateSummaryFromTraces();
440
+ render();
441
+ if (selectedId === t.id) renderDetail(t);
442
+ });
443
+ liveSocket.on('trace:done', (t) => {
444
+ traces[t.id] = t;
445
+ updateSummaryFromTraces();
446
+ render();
447
+ });
448
+
449
+ // Update session badge
450
+ const badge = document.getElementById('session-badge');
451
+ if (badge) badge.textContent = currentProjectId ? `Project: ${currentProjectId.replace('project:', '')}` : 'Session: —';
452
+ }
453
+
454
+ window.addEventListener('hashchange', () => {
455
+ navigateTo(getHashPage(), getHashProject());
324
456
  });
325
457
 
458
+ // ── Dashboard ──────────────────────────────────
459
+ function scheduleDashboardRefresh() {
460
+ if (dashboardRefreshPending) return;
461
+ dashboardRefreshPending = true;
462
+ setTimeout(() => { dashboardRefreshPending = false; loadDashboard(); }, 500);
463
+ }
464
+
465
+ async function loadDashboard() {
466
+ try {
467
+ const res = await fetch('/api/dashboard');
468
+ const data = await res.json();
469
+ renderDashboard(data);
470
+ } catch (e) {
471
+ console.error('Dashboard load failed:', e);
472
+ }
473
+ }
474
+
475
+ function renderDashboard(data) {
476
+ const totalsEl = document.getElementById('dash-totals');
477
+ totalsEl.innerHTML = `
478
+ <div class="dash-total-card">
479
+ <div class="dash-total-label">Projects</div>
480
+ <div class="dash-total-value">${data.totals.projects}</div>
481
+ </div>
482
+ <div class="dash-total-card">
483
+ <div class="dash-total-label">Sessions</div>
484
+ <div class="dash-total-value sessions">${data.totals.sessions}</div>
485
+ </div>
486
+ <div class="dash-total-card">
487
+ <div class="dash-total-label">Total Tokens</div>
488
+ <div class="dash-total-value tokens">${data.totals.tokens.toLocaleString()}</div>
489
+ </div>
490
+ <div class="dash-total-card">
491
+ <div class="dash-total-label">Total AI Credits</div>
492
+ <div class="dash-total-value credits">${fmtCredits(data.totals.credits)}</div>
493
+ </div>
494
+ `;
495
+
496
+ const projectsEl = document.getElementById('dash-projects');
497
+ if (!data.projects.length) {
498
+ projectsEl.innerHTML = `
499
+ <div class="dash-empty">
500
+ <div style="font-size:32px;margin-bottom:8px">📡</div>
501
+ <div style="font-size:14px;color:#e6edf3;margin-bottom:4px">No projects traced yet</div>
502
+ <p>Start the tracer with a project path:</p>
503
+ <p style="margin-top:8px"><code>copilot-tracer --project-path /path/to/repo</code></p>
504
+ </div>`;
505
+ return;
506
+ }
507
+
508
+ projectsEl.innerHTML = data.projects.map(p => {
509
+ const shortPath = p.localPath
510
+ ? p.localPath.replace(/^\/Users\/[^/]+/, '~')
511
+ : (p.repoUrl || p.path).replace(/^https?:\/\/github\.com\//, 'gh:').replace(/\.git$/, '');
512
+ const lastActive = p.lastActiveAt ? fmtDateTime(p.lastActiveAt) : '—';
513
+ const repoBadge = p.repoUrl ? `<div style="margin-top:6px;font-size:10px;color:#58a6ff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(p.repoUrl)}">${escHtml(p.repoUrl.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, ''))}</div>` : '';
514
+ return `
515
+ <div class="dash-project-card" onclick="location.hash='#/live?project=${encodeURIComponent(p.id)}'">
516
+ <div class="dash-project-path" title="${escHtml(p.localPath || p.repoUrl || p.path)}">${escHtml(shortPath)}</div>
517
+ ${repoBadge}
518
+ <div class="dash-project-stats">
519
+ <div class="dash-project-stat">
520
+ <span class="dash-project-stat-label">Sessions</span>
521
+ <span class="dash-project-stat-value">${p.sessionCount}</span>
522
+ </div>
523
+ <div class="dash-project-stat">
524
+ <span class="dash-project-stat-label">Tokens</span>
525
+ <span class="dash-project-stat-value tokens">${p.totalTokens.toLocaleString()}</span>
526
+ </div>
527
+ <div class="dash-project-stat">
528
+ <span class="dash-project-stat-label">AI Credits</span>
529
+ <span class="dash-project-stat-value credits">${fmtCredits(p.totalCredits)}</span>
530
+ </div>
531
+ </div>
532
+ <div class="dash-project-last-active">Last active: ${lastActive}</div>
533
+ </div>`;
534
+ }).join('');
535
+ }
536
+
537
+ // ── Live Tracer (Socket.io) ────────────────────
538
+ // Connection is managed by connectLiveSocket()
539
+
326
540
  function updateSummaryFromTraces() {
327
541
  const arr = Object.values(traces);
328
542
  summary = {
@@ -355,7 +569,6 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
355
569
  }
356
570
 
357
571
  function render() {
358
- // Stats bar
359
572
  if (summary) {
360
573
  document.getElementById('s-prompts').textContent = summary.totalEntries;
361
574
  document.getElementById('s-credits').textContent = fmtCredits(summary.totalCredits);
@@ -548,6 +761,16 @@ Duration: ${fmtDuration(t.durationMs)}
548
761
  function escHtml(s) {
549
762
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
550
763
  }
764
+
765
+ // ── Init ───────────────────────────────────────
766
+ const initPage = getHashPage();
767
+ const initProject = getHashProject();
768
+ if (initPage === 'live') {
769
+ connectLiveSocket(initProject);
770
+ } else {
771
+ loadDashboard();
772
+ }
773
+ navigateTo(initPage, initProject);
551
774
  </script>
552
775
  </body>
553
776
  </html>