ccanalyzer 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/package.json +1 -1
- package/src/public/app.js +134 -4
- package/src/public/index.html +4 -4
- package/src/public/style.css +62 -2
package/bin/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const { startServer } = require('../src/server');
|
|
|
5
5
|
const port = parseInt(process.env.PORT || '3737', 10);
|
|
6
6
|
|
|
7
7
|
startServer(port).then(({ url }) => {
|
|
8
|
-
console.log(`\n
|
|
8
|
+
console.log(`\n ccanalyzer running at ${url}\n`);
|
|
9
9
|
try {
|
|
10
10
|
// Try to open browser
|
|
11
11
|
import('open').then(m => m.default(url)).catch(() => {});
|
package/package.json
CHANGED
package/src/public/app.js
CHANGED
|
@@ -62,6 +62,27 @@ function showView(name) {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function pushUrl(params) {
|
|
66
|
+
const qs = new URLSearchParams(params).toString();
|
|
67
|
+
history.pushState(params, '', qs ? '?' + qs : location.pathname);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function restoreFromUrl() {
|
|
71
|
+
const p = new URLSearchParams(location.search);
|
|
72
|
+
const project = p.get('project');
|
|
73
|
+
const session = p.get('session');
|
|
74
|
+
if (project && session) return loadSessionDetail(encodeURIComponent(project), encodeURIComponent(session));
|
|
75
|
+
if (project) return loadSessions(encodeURIComponent(project));
|
|
76
|
+
loadDashboard();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
window.addEventListener('popstate', e => {
|
|
80
|
+
const d = e.state || {};
|
|
81
|
+
if (d.session && d.project) loadSessionDetail(encodeURIComponent(d.project), encodeURIComponent(d.session));
|
|
82
|
+
else if (d.project) loadSessions(encodeURIComponent(d.project));
|
|
83
|
+
else loadDashboard();
|
|
84
|
+
});
|
|
85
|
+
|
|
65
86
|
function setBreadcrumb(parts) {
|
|
66
87
|
$('breadcrumb').innerHTML = parts.map(p => `<div style="color:var(--text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p)}</div>`).join('');
|
|
67
88
|
}
|
|
@@ -75,6 +96,7 @@ async function api(path) {
|
|
|
75
96
|
|
|
76
97
|
/* ── Dashboard ── */
|
|
77
98
|
async function loadDashboard() {
|
|
99
|
+
pushUrl({});
|
|
78
100
|
showView('dashboard');
|
|
79
101
|
setBreadcrumb(['Dashboard']);
|
|
80
102
|
const container = $('view-dashboard');
|
|
@@ -178,6 +200,7 @@ function initActivityChart() {
|
|
|
178
200
|
/* ── Sessions list ── */
|
|
179
201
|
async function loadSessions(dirNameEncoded) {
|
|
180
202
|
const dirName = decodeURIComponent(dirNameEncoded);
|
|
203
|
+
pushUrl({ project: dirName });
|
|
181
204
|
showView('sessions');
|
|
182
205
|
|
|
183
206
|
if (!state.projects) {
|
|
@@ -230,6 +253,7 @@ async function loadSessions(dirNameEncoded) {
|
|
|
230
253
|
async function loadSessionDetail(dirNameEncoded, fileEncoded) {
|
|
231
254
|
const dirName = decodeURIComponent(dirNameEncoded);
|
|
232
255
|
const file = decodeURIComponent(fileEncoded);
|
|
256
|
+
pushUrl({ project: dirName, session: file });
|
|
233
257
|
showView('session-detail');
|
|
234
258
|
const container = $('view-session-detail');
|
|
235
259
|
container.innerHTML = '<div style="padding:40px;text-align:center;color:var(--text3)">Chargement...</div>';
|
|
@@ -264,6 +288,30 @@ function renderSessionDetail(session, dirName, file) {
|
|
|
264
288
|
? new Date(lastTimestamp) - new Date(firstTimestamp) : null;
|
|
265
289
|
const hasAgents = agents && agents.length > 0;
|
|
266
290
|
|
|
291
|
+
// Aggregate MCPs and skills across all messages
|
|
292
|
+
const sessionMcps = new Set();
|
|
293
|
+
const sessionSkills = new Set();
|
|
294
|
+
for (const msg of session.messages) {
|
|
295
|
+
if (msg.type !== 'assistant') continue;
|
|
296
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
297
|
+
for (const block of content) {
|
|
298
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
299
|
+
if (block.name.startsWith('mcp__')) {
|
|
300
|
+
const parts = block.name.split('__');
|
|
301
|
+
const server = (parts[1] || '').replace(/^claude_ai_/, '').replace(/_/g, ' ');
|
|
302
|
+
if (server) sessionMcps.add(server);
|
|
303
|
+
} else if (block.name === 'Skill' && block.input?.skill) {
|
|
304
|
+
sessionSkills.add(block.input.skill);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const mcpBar = sessionMcps.size > 0
|
|
309
|
+
? `<div class="session-tools-bar">${[...sessionMcps].map(s => `<span class="usage-chip"><span class="tag-mcp">mcp</span>${escHtml(s)}</span>`).join('')}</div>`
|
|
310
|
+
: '';
|
|
311
|
+
const skillBar = sessionSkills.size > 0
|
|
312
|
+
? `<div class="session-tools-bar">${[...sessionSkills].map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}</div>`
|
|
313
|
+
: '';
|
|
314
|
+
|
|
267
315
|
container.innerHTML = `
|
|
268
316
|
<button class="back-btn" onclick="loadSessions('${encodeURIComponent(dirName)}')">← Sessions</button>
|
|
269
317
|
<div class="page-header">
|
|
@@ -278,6 +326,7 @@ function renderSessionDetail(session, dirName, file) {
|
|
|
278
326
|
${cwd ? `<div class="meta-item"><div class="meta-label">Répertoire</div><div class="meta-value mono">${escHtml(cwd.replace('/home/olivier-j/', '~/'))}</div></div>` : ''}
|
|
279
327
|
${gitBranch && gitBranch !== 'HEAD' ? `<div class="meta-item"><div class="meta-label">Branche</div><div class="meta-value mono" style="color:var(--green)">${escHtml(gitBranch)}</div></div>` : ''}
|
|
280
328
|
</div>
|
|
329
|
+
${mcpBar}${skillBar}
|
|
281
330
|
|
|
282
331
|
<div class="token-bar">
|
|
283
332
|
<div class="token-item"><div class="token-label">Input</div><div class="token-value input">${fmt(totalUsage.input)}</div></div>
|
|
@@ -821,15 +870,84 @@ function renderMessages(messages, ctx) {
|
|
|
821
870
|
return true;
|
|
822
871
|
});
|
|
823
872
|
if (!filtered.length) return '<div class="empty"><p>Aucun message</p></div>';
|
|
824
|
-
|
|
873
|
+
|
|
874
|
+
// Track active skill/agent context across messages
|
|
875
|
+
let activeAgent = null;
|
|
876
|
+
return filtered.map((m, i) => {
|
|
877
|
+
if (m.type === 'assistant') {
|
|
878
|
+
const content = Array.isArray(m.content) ? m.content : [];
|
|
879
|
+
for (const block of content) {
|
|
880
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
881
|
+
if (block.name === 'Skill') {
|
|
882
|
+
activeAgent = { kind: 'skill', name: block.input?.skill || '?' };
|
|
883
|
+
} else if (block.name === 'Agent') {
|
|
884
|
+
const name = block.input?.description || block.input?.subagent_type || 'agent';
|
|
885
|
+
activeAgent = { kind: 'agent', name: name.slice(0, 40) };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return renderMessage(m, i, ctx, activeAgent);
|
|
890
|
+
}).join('');
|
|
825
891
|
}
|
|
826
892
|
|
|
827
|
-
function renderMessage(m, i, ctx) {
|
|
893
|
+
function renderMessage(m, i, ctx, activeAgent) {
|
|
828
894
|
const isUser = m.type === 'user';
|
|
829
895
|
const isAgent = m.isSidechain;
|
|
830
896
|
const collapseByDefault = !isUser && i > 0;
|
|
831
897
|
const pfx = ctx === 'modal' ? 'modal-' : '';
|
|
832
898
|
|
|
899
|
+
// Extract tool info for assistant messages (used in header hint + usage chips)
|
|
900
|
+
const msgToolNames = [];
|
|
901
|
+
const mcpServers = [];
|
|
902
|
+
const skillsUsed = [];
|
|
903
|
+
if (!isUser) {
|
|
904
|
+
const contentArr = Array.isArray(m.content) ? m.content : [];
|
|
905
|
+
for (const block of contentArr) {
|
|
906
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
907
|
+
msgToolNames.push(block.name);
|
|
908
|
+
if (block.name.startsWith('mcp__')) {
|
|
909
|
+
const parts = block.name.split('__');
|
|
910
|
+
const server = (parts[1] || '').replace(/^claude_ai_/, '').replace(/_/g, ' ');
|
|
911
|
+
if (server && !mcpServers.includes(server)) mcpServers.push(server);
|
|
912
|
+
} else if (block.name === 'Skill' && block.input?.skill) {
|
|
913
|
+
const sn = block.input.skill;
|
|
914
|
+
if (!skillsUsed.includes(sn)) skillsUsed.push(sn);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Build a human-readable hint for the collapsed header
|
|
919
|
+
let toolHint = '';
|
|
920
|
+
if (!isUser && msgToolNames.length > 0) {
|
|
921
|
+
const hintParts = [];
|
|
922
|
+
// Agent spawns: show description
|
|
923
|
+
const contentArr2 = Array.isArray(m.content) ? m.content : [];
|
|
924
|
+
for (const block of contentArr2) {
|
|
925
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
926
|
+
if (block.name === 'Agent') {
|
|
927
|
+
const desc = block.input?.description || block.input?.subagent_type || 'agent';
|
|
928
|
+
hintParts.push(`⬡ ${desc.slice(0, 40)}`);
|
|
929
|
+
} else if (block.name === 'Skill') {
|
|
930
|
+
hintParts.push(`skill:${block.input?.skill || '?'}`);
|
|
931
|
+
} else if (block.name.startsWith('mcp__')) {
|
|
932
|
+
const parts = block.name.split('__');
|
|
933
|
+
const server = (parts[1] || '').replace(/^claude_ai_/, '');
|
|
934
|
+
const tool = (parts[2] || '').replace(/^[a-z]+-/, '');
|
|
935
|
+
if (!hintParts.some(p => p.startsWith(`mcp:${server}`)))
|
|
936
|
+
hintParts.push(`mcp:${server}/${tool}`);
|
|
937
|
+
} else if (!['Read','Write','Edit','Bash','WebSearch','WebFetch','ToolSearch'].includes(block.name)) {
|
|
938
|
+
hintParts.push(block.name);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Fallback to common tools if nothing notable
|
|
942
|
+
if (!hintParts.length) {
|
|
943
|
+
const common = msgToolNames.slice(0, 3);
|
|
944
|
+
hintParts.push(...common);
|
|
945
|
+
if (msgToolNames.length > 3) hintParts.push(`+${msgToolNames.length - 3}`);
|
|
946
|
+
}
|
|
947
|
+
toolHint = hintParts.slice(0, 3).join(' ');
|
|
948
|
+
if (hintParts.length > 3) toolHint += ` +${hintParts.length - 3}`;
|
|
949
|
+
}
|
|
950
|
+
|
|
833
951
|
let bodyHtml = '';
|
|
834
952
|
if (isUser) {
|
|
835
953
|
if (typeof m.content === 'string') {
|
|
@@ -880,24 +998,36 @@ function renderMessage(m, i, ctx) {
|
|
|
880
998
|
${u.cache_creation_input_tokens ? `<span class="usage-chip">cache↑: <span class="cw">${fmt(u.cache_creation_input_tokens)}</span></span>` : ''}
|
|
881
999
|
${u.cache_read_input_tokens ? `<span class="usage-chip">cache↓: <span class="cr">${fmt(u.cache_read_input_tokens)}</span></span>` : ''}
|
|
882
1000
|
${m.model ? `<span class="usage-chip" style="color:var(--accent)">${escHtml(modelShort(m.model))}</span>` : ''}
|
|
1001
|
+
${mcpServers.map(s => `<span class="usage-chip"><span class="tag-mcp">mcp</span>${escHtml(s)}</span>`).join('')}
|
|
1002
|
+
${skillsUsed.map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}
|
|
883
1003
|
</div>`;
|
|
884
1004
|
}
|
|
885
1005
|
|
|
886
1006
|
const ts = m.timestamp ? fmtTime(m.timestamp) : '';
|
|
887
1007
|
const msgId = `${pfx}msg-${i}`;
|
|
888
1008
|
|
|
1009
|
+
let agentBadge = '';
|
|
1010
|
+
if (!isUser && activeAgent) {
|
|
1011
|
+
const icon = activeAgent.kind === 'skill' ? '⚡' : '⬡';
|
|
1012
|
+
const col = activeAgent.kind === 'skill' ? 'var(--purple)' : 'var(--teal)';
|
|
1013
|
+
agentBadge = `<span class="msg-agent-badge" style="color:${col}">${icon} ${escHtml(activeAgent.name)}</span>`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
889
1016
|
return `
|
|
890
1017
|
<div class="message ${isAgent ? 'sidechain' : ''} ${collapseByDefault ? 'collapsed' : ''}" id="${msgId}" data-uuid="${escHtml(m.uuid || '')}">
|
|
891
1018
|
<div class="message-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
892
1019
|
<span class="msg-role ${isUser ? 'user' : 'assistant'}">${isUser ? '👤 User' : '🤖 AI'}</span>
|
|
1020
|
+
${agentBadge}
|
|
893
1021
|
${isAgent ? '<span class="badge badge-sidechain">agent</span>' : ''}
|
|
1022
|
+
${toolHint ? `<span class="msg-tool-hint">${escHtml(toolHint)}</span>` : ''}
|
|
894
1023
|
<div class="msg-meta">
|
|
895
1024
|
${ts ? `<span>${ts}</span>` : ''}
|
|
896
1025
|
${m.stopReason ? `<span style="color:var(--text3)">${escHtml(m.stopReason)}</span>` : ''}
|
|
897
1026
|
</div>
|
|
898
1027
|
<span class="msg-chevron">▼</span>
|
|
899
1028
|
</div>
|
|
900
|
-
<div class="message-body">${bodyHtml}
|
|
1029
|
+
<div class="message-body">${bodyHtml}</div>
|
|
1030
|
+
${usageHtml ? `<div class="message-stats">${usageHtml}</div>` : ''}
|
|
901
1031
|
</div>`;
|
|
902
1032
|
}
|
|
903
1033
|
|
|
@@ -920,4 +1050,4 @@ window.setMsgFilter = setMsgFilter;
|
|
|
920
1050
|
window.toggleTimeline = toggleTimeline;
|
|
921
1051
|
window.closeAgentModal = closeAgentModal;
|
|
922
1052
|
|
|
923
|
-
|
|
1053
|
+
restoreFromUrl();
|
package/src/public/index.html
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>
|
|
7
|
-
<link rel="stylesheet" href="style.css" />
|
|
6
|
+
<title>ccanalyzer</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css?v=6" />
|
|
8
8
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<nav id="sidebar">
|
|
13
13
|
<div class="logo">
|
|
14
14
|
<span class="logo-icon">◈</span>
|
|
15
|
-
<span class="logo-text">
|
|
15
|
+
<span class="logo-text">ccanalyzer</span>
|
|
16
16
|
</div>
|
|
17
17
|
<div id="nav-menu">
|
|
18
18
|
<a href="#" class="nav-item active" data-view="dashboard">
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
<div class="spinner"></div>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
-
<script src="app.js"></script>
|
|
36
|
+
<script src="app.js?v=6"></script>
|
|
37
37
|
</body>
|
|
38
38
|
</html>
|
package/src/public/style.css
CHANGED
|
@@ -241,7 +241,7 @@ tr:hover td { background: var(--bg3); cursor: pointer; }
|
|
|
241
241
|
display: flex;
|
|
242
242
|
flex-wrap: wrap;
|
|
243
243
|
gap: 12px;
|
|
244
|
-
margin-bottom:
|
|
244
|
+
margin-bottom: 14px;
|
|
245
245
|
padding: 16px 20px;
|
|
246
246
|
background: var(--bg2);
|
|
247
247
|
border: 1px solid var(--border);
|
|
@@ -262,7 +262,7 @@ tr:hover td { background: var(--bg3); cursor: pointer; }
|
|
|
262
262
|
background: var(--bg2);
|
|
263
263
|
border: 1px solid var(--border);
|
|
264
264
|
border-radius: var(--radius2);
|
|
265
|
-
margin-bottom:
|
|
265
|
+
margin-bottom: 14px;
|
|
266
266
|
flex-wrap: wrap;
|
|
267
267
|
}
|
|
268
268
|
.token-item { display: flex; flex-direction: column; gap: 3px; }
|
|
@@ -335,6 +335,31 @@ tr:hover td { background: var(--bg3); cursor: pointer; }
|
|
|
335
335
|
}
|
|
336
336
|
.message.collapsed .message-body { display: none; }
|
|
337
337
|
|
|
338
|
+
.message-stats {
|
|
339
|
+
padding: 8px 16px 10px;
|
|
340
|
+
background: var(--bg2);
|
|
341
|
+
border-top: 1px solid var(--border);
|
|
342
|
+
}
|
|
343
|
+
.message-stats .usage-inline {
|
|
344
|
+
margin-top: 0;
|
|
345
|
+
padding-top: 0;
|
|
346
|
+
border-top: none;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.msg-agent-badge {
|
|
350
|
+
font-family: var(--font);
|
|
351
|
+
font-size: 10px;
|
|
352
|
+
font-weight: 600;
|
|
353
|
+
padding: 1px 6px;
|
|
354
|
+
border-radius: 4px;
|
|
355
|
+
background: #1a1e28;
|
|
356
|
+
flex-shrink: 0;
|
|
357
|
+
white-space: nowrap;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
text-overflow: ellipsis;
|
|
360
|
+
max-width: 200px;
|
|
361
|
+
}
|
|
362
|
+
|
|
338
363
|
.msg-text {
|
|
339
364
|
color: var(--text);
|
|
340
365
|
line-height: 1.65;
|
|
@@ -405,6 +430,41 @@ tr:hover td { background: var(--bg3); cursor: pointer; }
|
|
|
405
430
|
.usage-chip .cw { color: var(--yellow); }
|
|
406
431
|
.usage-chip .cr { color: var(--teal); }
|
|
407
432
|
|
|
433
|
+
.tag-mcp, .tag-skill {
|
|
434
|
+
display: inline-block;
|
|
435
|
+
font-size: 9px;
|
|
436
|
+
font-weight: 700;
|
|
437
|
+
text-transform: uppercase;
|
|
438
|
+
letter-spacing: 0.5px;
|
|
439
|
+
padding: 1px 4px;
|
|
440
|
+
border-radius: 3px;
|
|
441
|
+
margin-right: 4px;
|
|
442
|
+
}
|
|
443
|
+
.tag-mcp { background: #1a2e2a; color: var(--teal); }
|
|
444
|
+
.tag-skill { background: #221a30; color: var(--purple); }
|
|
445
|
+
|
|
446
|
+
.msg-tool-hint {
|
|
447
|
+
font-family: var(--font);
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
color: var(--text3);
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
text-overflow: ellipsis;
|
|
452
|
+
white-space: nowrap;
|
|
453
|
+
max-width: 320px;
|
|
454
|
+
flex-shrink: 1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.session-tools-bar {
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-wrap: wrap;
|
|
460
|
+
gap: 8px;
|
|
461
|
+
padding: 10px 16px;
|
|
462
|
+
background: var(--bg2);
|
|
463
|
+
border: 1px solid var(--border);
|
|
464
|
+
border-radius: var(--radius);
|
|
465
|
+
margin-bottom: 14px;
|
|
466
|
+
}
|
|
467
|
+
|
|
408
468
|
/* ── Back button ── */
|
|
409
469
|
.back-btn {
|
|
410
470
|
display: inline-flex;
|