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/README.md +136 -159
- package/dist/cli.js +95 -76
- package/dist/db.js +166 -3
- package/dist/otlpReceiver.js +222 -15
- package/dist/setup.js +81 -14
- package/dist/webServer.js +39 -8
- package/package.json +2 -2
- package/web/index.html +296 -73
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
</
|
|
247
|
-
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
409
|
+
function getHashPage() {
|
|
410
|
+
const hash = location.hash.replace('#/', '').split('?')[0];
|
|
411
|
+
return pages.includes(hash) ? hash : 'dashboard';
|
|
412
|
+
}
|
|
312
413
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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>
|