copilot-tracer 1.0.3 → 1.0.5

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/web/index.html CHANGED
@@ -5,10 +5,142 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
6
  <title>Copilot Tracer</title>
7
7
  <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
8
+ <script type="module">
9
+ import * as webllm from 'https://esm.run/@mlc-ai/web-llm';
10
+
11
+ let refineOpen = false;
12
+ let engine = null;
13
+
14
+ const tok = t => Math.ceil((t || '').length / 4);
15
+
16
+ function analyzePrompt(t) {
17
+ const issues = [], suggestions = [];
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)))
19
+ issues.push({ type:'warn', msg:'Filler / polite noise (please, can you, thank you…)' });
20
+ if ([/\bI think\b/,/\bmaybe\b/,/\bkind of\b/,/\bsort of\b/].some(r => r.test(t)))
21
+ issues.push({ type:'warn', msg:'Hedging language reduces precision (I think, maybe, kind of…)' });
22
+ if (/\bhelp me with\b|\bhelp me understand\b/i.test(t))
23
+ issues.push({ type:'warn', msg:'Indirect phrasing — use an imperative verb: Explain / List / Generate / Analyze' });
24
+ if (/\bmake it (?:good|nice|better|great)\b/i.test(t))
25
+ issues.push({ type:'warn', msg:'Vague quality instruction — specify criteria (concise, bullet-list, ≤200 words…)' });
26
+ const hasFormat = /\bjson\b|\bmarkdown\b|\bbullet\b|\bnumbered\b|\bstep.by.step\b/i.test(t);
27
+ if (!hasFormat && t.length > 60) issues.push({ type:'info', msg:'No output format — add: "Respond in bullet points / JSON / numbered steps"' });
28
+ const hasRole = /\byou are\b|\bact as\b|\bexpert\b|\bsenior\b/i.test(t);
29
+ if (!hasRole && t.length > 80) issues.push({ type:'info', msg:'No role/persona — prepend "You are a <expert>." for better quality' });
30
+ if (!/\bstep by step\b|\bthink\b/i.test(t) && /\bdebug\b|\banalyze\b|\bcompare\b|\bdesign\b/i.test(t))
31
+ issues.push({ type:'info', msg:'Complex task — add "Think step by step." for chain-of-thought reasoning' });
32
+ if (!issues.length) issues.push({ type:'info', msg:'No obvious issues — prompt looks well-structured' });
33
+ if (!hasRole && t.length > 80) suggestions.push('Add a role: "You are a senior software engineer."');
34
+ if (!hasFormat && t.length > 60) suggestions.push('Specify output: "Respond in numbered steps."');
35
+ return { issues, suggestions };
36
+ }
37
+
38
+ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
39
+
40
+ window.toggleRefine = async function(prompt) {
41
+ const panel = document.getElementById('refine-panel');
42
+ if (!panel) return;
43
+ if (refineOpen) { panel.classList.remove('open'); refineOpen = false; return; }
44
+ refineOpen = true;
45
+ panel.classList.add('open');
46
+
47
+ const origTok = tok(prompt);
48
+ const { issues, suggestions } = analyzePrompt(prompt);
49
+
50
+ const issueHtml = issues.map(i => `<div class="refine-issue ${i.type}">⚠ ${escHtml(i.msg)}</div>`).join('');
51
+ const suggHtml = suggestions.length
52
+ ? `<div class=refine-section-title>Quick Suggestions</div><div class=refine-suggestions>${suggestions.map(s=>`<div class=refine-suggestion>💡 ${escHtml(s)}</div>`).join('')}</div>`
53
+ : '';
54
+
55
+ panel.innerHTML = `
56
+ <div class=refine-section-title>Issues Found</div>
57
+ <div class=refine-issues>${issueHtml}</div>
58
+ ${suggHtml}
59
+ <div class=refine-section-title>Optimized Prompt</div>
60
+ <div id=refine-optimizing style="color:#8b949e;font-size:12px;padding:6px 0">
61
+ <span class=refine-spinner></span> Optimizing with WebLLM…
62
+ <div id=refine-progress style="margin-top:5px;font-size:10px;color:#58a6ff"></div>
63
+ </div>
64
+ <div class=refine-optimized id=refined-text style="display:none"></div>
65
+ <div id=refine-actions style="display:none">
66
+ <button class="refine-copy-btn" onclick="copyRefined()">📋 Copy optimized prompt</button>
67
+ <div class="refine-stats" id=refine-stats></div>
68
+ </div>`;
69
+
70
+ const progressEl = () => document.getElementById('refine-progress');
71
+ const optimizingEl = document.getElementById('refine-optimizing');
72
+ const refinedEl = document.getElementById('refined-text');
73
+ const actionsEl = document.getElementById('refine-actions');
74
+
75
+ try {
76
+ if (!engine) {
77
+ progressEl() && (progressEl().textContent = 'Loading model… (first run ~650 MB, cached after)');
78
+ engine = await webllm.CreateMLCEngine('Llama-3.2-1B-Instruct-q4f16_1-MLC', {
79
+ initProgressCallback: p => { const el = progressEl(); if (el) el.textContent = p.text || ''; }
80
+ });
81
+ }
82
+ if (progressEl()) progressEl().textContent = 'Generating…';
83
+
84
+ const sys = `You are a prompt engineering expert. Rewrite the user prompt by:
85
+ 1. Adding a role ("You are a <expert>.") if missing
86
+ 2. Replacing indirect/hedging phrases with direct imperatives (Explain / List / Generate)
87
+ 3. Adding output format (numbered steps / bullet list / JSON) if missing
88
+ 4. Adding "Think step by step." for complex tasks (debug, analyze, compare)
89
+ 5. Removing filler words (please, can you, thank you, maybe, I think)
90
+ 6. Adding constraints (language, length, audience) if missing
91
+ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
92
+
93
+ const reply = await engine.chat.completions.create({
94
+ messages: [{ role:'system', content:sys }, { role:'user', content:prompt }],
95
+ temperature: 0.3,
96
+ max_tokens: 512,
97
+ });
98
+
99
+ const optimized = (reply.choices[0]?.message?.content || '').trim();
100
+ const newTok = tok(optimized);
101
+ const pct = origTok > 0 ? Math.max(0, Math.round((1 - newTok / origTok) * 100)) : 0;
102
+ const col = pct >= 20 ? '#7ee787' : pct >= 5 ? '#d29922' : '#8b949e';
103
+
104
+ optimizingEl.style.display = 'none';
105
+ refinedEl.textContent = optimized;
106
+ refinedEl.style.display = 'block';
107
+ actionsEl.style.display = 'block';
108
+ document.getElementById('refine-stats').innerHTML = `
109
+ <div class=refine-stat><div class=rv style=color:${col}>${pct > 0 ? '-'+pct+'%' : '0%'}</div><div class=rl>Input Saving</div></div>
110
+ <div class=refine-stat><div class=rv style=color:#8b949e>${origTok} → ${newTok}</div><div class=rl>Approx Tokens</div></div>`;
111
+ } catch(e) {
112
+ optimizingEl.innerHTML = `<span style="color:#f85149">⚠ WebLLM error: ${escHtml(e.message)}</span>`;
113
+ }
114
+ };
115
+
116
+ window.copyRefined = function() {
117
+ const el = document.getElementById('refined-text');
118
+ if (!el) return;
119
+ navigator.clipboard.writeText(el.innerText).then(() => {
120
+ const btn = document.querySelector('.refine-copy-btn');
121
+ if (btn) { btn.textContent = '✅ Copied!'; setTimeout(() => btn.textContent = '📋 Copy optimized prompt', 1500); }
122
+ });
123
+ };
124
+ </script>
8
125
  <style>
9
126
  * { box-sizing: border-box; margin: 0; padding: 0; }
10
- body { background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', system-ui, monospace; font-size: 13px; }
11
-
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) ────────────────────── */
12
144
  header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
13
145
  header h1 { color: #58a6ff; font-size: 16px; font-weight: 700; }
14
146
  .badge { background: #21262d; border: 1px solid #30363d; border-radius: 12px; padding: 2px 10px; font-size: 11px; color: #8b949e; }
@@ -16,7 +148,8 @@
16
148
  #status-dot { width: 8px; height: 8px; border-radius: 50%; background: #3fb950; display: inline-block; animation: pulse 2s infinite; }
17
149
  @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
18
150
 
19
- .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; }
20
153
  .stat { display: flex; flex-direction: column; }
21
154
  .stat-label { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
22
155
  .stat-value { font-size: 15px; font-weight: 700; color: #e6edf3; }
@@ -25,9 +158,10 @@
25
158
  .stat-value.skills { color: #d2a8ff; }
26
159
  .stat-value.mcps { color: #56d364; }
27
160
 
28
- .main { display: flex; height: calc(100vh - 100px); }
161
+ /* ── Main split (live tracer) ────────────────── */
162
+ .main { display: flex; flex: 1; overflow: hidden; }
29
163
  .table-panel { flex: 1; overflow: auto; }
30
- .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; }
31
165
  .detail-panel.open { display: flex; }
32
166
 
33
167
  table { width: 100%; border-collapse: collapse; }
@@ -57,7 +191,7 @@
57
191
  .pill-mcp { background: #0a3622; color: #56d364; }
58
192
  .pill:hover { opacity: 0.8; filter: brightness(1.2); }
59
193
 
60
- /* Detail panel */
194
+ /* ── Detail panel ────────────────────────────── */
61
195
  .detail-header { padding: 14px 16px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; }
62
196
  .detail-header h3 { color: #58a6ff; font-size: 13px; }
63
197
  .close-btn { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 18px; line-height: 1; }
@@ -70,7 +204,7 @@
70
204
  .detail-box.response { color: #56d364; }
71
205
  .detail-box.prompt { color: #e6edf3; max-height: none; overflow: visible; }
72
206
 
73
- /* Call graph */
207
+ /* ── Call graph ──────────────────────────────── */
74
208
  .call-tree { font-family: monospace; font-size: 12px; }
75
209
  .call-node { padding: 4px 0; }
76
210
  .call-node-header { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 4px 6px; border-radius: 4px; }
@@ -83,7 +217,32 @@
83
217
  .call-io .label { color: #58a6ff; margin-right: 4px; }
84
218
  .call-io.error { color: #f85149; }
85
219
 
86
- /* Token detail modal */
220
+ /* ── Refine panel ────────────────────────────── */
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; }
222
+ .refine-btn:hover { background: #238636; color: #fff; }
223
+ .refine-panel { margin-top: 12px; background: #0d1117; border: 1px solid #238636; border-radius: 8px; padding: 14px; display: none; }
224
+ .refine-panel.open { display: block; }
225
+ .refine-section-title { font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #8b949e; margin-bottom: 6px; margin-top: 12px; }
226
+ .refine-section-title:first-child { margin-top: 0; }
227
+ .refine-issues { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
228
+ .refine-issue { font-size: 11px; padding: 4px 8px; border-radius: 4px; background: #21262d; border-left: 3px solid; }
229
+ .refine-issue.warn { border-color: #d29922; color: #d29922; }
230
+ .refine-issue.info { border-color: #58a6ff; color: #8b949e; }
231
+ .refine-optimized { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: #7ee787; max-height: 260px; overflow: auto; }
232
+ .refine-stats { display: flex; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
233
+ .refine-stat { background: #21262d; border-radius: 6px; padding: 6px 12px; text-align: center; }
234
+ .refine-stat .rv { font-size: 18px; font-weight: 700; }
235
+ .refine-stat .rl { font-size: 10px; color: #8b949e; margin-top: 1px; }
236
+ .refine-copy-btn { margin-top: 10px; background: #21262d; border: 1px solid #30363d; color: #8b949e; border-radius: 6px; padding: 4px 12px; font-size: 11px; cursor: pointer; }
237
+ .refine-copy-btn:hover { color: #e6edf3; border-color: #58a6ff; }
238
+ .refine-techniques { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
239
+ .refine-tag { background: #0d2230; border: 1px solid #1f6feb; color: #58a6ff; border-radius: 10px; padding: 2px 9px; font-size: 10px; }
240
+ .refine-suggestions { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
241
+ .refine-suggestion { font-size: 11px; padding: 4px 8px; border-radius: 4px; background: #0d2230; border-left: 3px solid #1f6feb; color: #8b949e; }
242
+ @keyframes refine-spin { to { transform: rotate(360deg); } }
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 ───────────────────────────────────── */
87
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; }
88
247
  .modal-overlay.open { display: flex; }
89
248
  .modal { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; width: 560px; max-height: 80vh; overflow: auto; }
@@ -91,58 +250,118 @@
91
250
  .modal-close { float: right; background: none; border: none; color: #8b949e; cursor: pointer; font-size: 20px; margin-top: -4px; }
92
251
 
93
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; }
94
282
  </style>
95
283
  </head>
96
284
  <body>
97
285
 
98
- <header>
99
- <span id="status-dot"></span>
100
- <h1>🤖 Copilot Tracer</h1>
101
- <span class="badge live">LIVE</span>
102
- <span class="badge" id="session-badge">Session: —</span>
103
- </header>
104
-
105
- <div class="stats-bar">
106
- <div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
107
- <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>
108
- <div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
109
- <div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
110
- <div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
111
- <div class="stat"><span class="stat-label">Skill Calls</span><span class="stat-value skills" id="s-skills">0</span></div>
112
- <div class="stat"><span class="stat-label">Agent Calls</span><span class="stat-value tokens" id="s-agents">0</span></div>
113
- <div class="stat"><span class="stat-label">MCP Calls</span><span class="stat-value mcps" id="s-mcps">0</span></div>
114
- <div class="stat"><span class="stat-label">Total Duration</span><span class="stat-value" id="s-duration">0s</span></div>
115
- </div>
116
-
117
- <div class="main">
118
- <div class="table-panel">
119
- <table>
120
- <thead>
121
- <tr>
122
- <th>Date / Time</th>
123
- <th>Prompt</th>
124
- <th>AI Credits</th>
125
- <th>Duration</th>
126
- <th>Cached Tokens</th>
127
- <th>Written Tokens</th>
128
- <th>Reasoning Tokens</th>
129
- <th>Skills</th>
130
- <th>Agents</th>
131
- <th>MCPs</th>
132
- </tr>
133
- </thead>
134
- <tbody id="trace-tbody">
135
- <tr><td colspan="10" class="empty">Waiting for Copilot CLI activity...</td></tr>
136
- </tbody>
137
- </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>
138
311
  </div>
139
312
 
140
- <div class="detail-panel" id="detail-panel">
141
- <div class="detail-header">
142
- <h3 id="detail-title">Trace Detail</h3>
143
- <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>
144
364
  </div>
145
- <div class="detail-body" id="detail-body"></div>
146
365
  </div>
147
366
  </div>
148
367
 
@@ -156,30 +375,168 @@
156
375
  </div>
157
376
 
158
377
  <script>
378
+ // ── Routing ────────────────────────────────────
379
+ const pages = ['dashboard', 'live'];
159
380
  const socket = io();
160
381
  let traces = {};
161
382
  let summary = null;
162
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
+ }
163
408
 
164
- socket.on('init', (data) => {
165
- data.traces.forEach(t => traces[t.id] = t);
166
- summary = data.summary;
167
- render();
168
- });
409
+ function getHashPage() {
410
+ const hash = location.hash.replace('#/', '').split('?')[0];
411
+ return pages.includes(hash) ? hash : 'dashboard';
412
+ }
169
413
 
170
- socket.on('trace:update', (t) => {
171
- traces[t.id] = t;
172
- updateSummaryFromTraces();
173
- render();
174
- if (selectedId === t.id) renderDetail(t);
175
- });
414
+ function getHashProject() {
415
+ const match = location.hash.match(/project=([^&]+)/);
416
+ return match ? decodeURIComponent(match[1]) : null;
417
+ }
176
418
 
177
- socket.on('trace:done', (t) => {
178
- traces[t.id] = t;
179
- updateSummaryFromTraces();
180
- 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());
181
456
  });
182
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
+
183
540
  function updateSummaryFromTraces() {
184
541
  const arr = Object.values(traces);
185
542
  summary = {
@@ -212,7 +569,6 @@
212
569
  }
213
570
 
214
571
  function render() {
215
- // Stats bar
216
572
  if (summary) {
217
573
  document.getElementById('s-prompts').textContent = summary.totalEntries;
218
574
  document.getElementById('s-credits').textContent = fmtCredits(summary.totalCredits);
@@ -265,12 +621,15 @@
265
621
  }
266
622
 
267
623
  function renderDetail(t) {
624
+ refineOpen = false;
268
625
  document.getElementById('detail-title').textContent = truncate(t.prompt || 'Trace Detail', 50);
269
626
  const body = document.getElementById('detail-body');
270
627
  body.innerHTML = `
271
628
  <div class="detail-section">
272
629
  <h4>Prompt</h4>
273
630
  <div class="detail-box prompt">${escHtml(t.prompt||'')}</div>
631
+ <button class="refine-btn" onclick="toggleRefine(${JSON.stringify(t.prompt||'').replace(/</g,'\\u003c').replace(/"/g,'&quot;')})">✨ Refine Prompt</button>
632
+ <div class="refine-panel" id="refine-panel"></div>
274
633
  </div>
275
634
  ${t.reasoning ? `
276
635
  <div class="detail-section">
@@ -402,6 +761,16 @@ Duration: ${fmtDuration(t.durationMs)}
402
761
  function escHtml(s) {
403
762
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
404
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);
405
774
  </script>
406
775
  </body>
407
776
  </html>
File without changes