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/README.md +119 -123
- package/dist/cli.js +95 -76
- package/dist/db.js +166 -3
- package/dist/otlpReceiver.js +34 -16
- package/dist/setup.js +35 -12
- package/dist/webServer.js +86 -8
- package/package.json +2 -2
- package/web/index.html +438 -69
- package/web/index.html.patch +0 -0
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
</
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
409
|
+
function getHashPage() {
|
|
410
|
+
const hash = location.hash.replace('#/', '').split('?')[0];
|
|
411
|
+
return pages.includes(hash) ? hash : 'dashboard';
|
|
412
|
+
}
|
|
169
413
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,'"')})">✨ 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|