claude-code-templates 1.26.1 → 1.26.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.26.1",
3
+ "version": "1.26.3",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -782,6 +782,15 @@
782
782
  stroke: rgba(251, 146, 60, 1);
783
783
  }
784
784
 
785
+ .action-btn.analytics-btn:hover {
786
+ background: rgba(139, 92, 246, 0.15);
787
+ color: rgba(139, 92, 246, 1);
788
+ }
789
+
790
+ .action-btn.analytics-btn:hover svg {
791
+ stroke: rgba(139, 92, 246, 1);
792
+ }
793
+
785
794
  /* Conversations list */
786
795
  .conversations-list {
787
796
  flex: 1;
@@ -2067,6 +2076,14 @@
2067
2076
  </svg>
2068
2077
  <span>Search</span>
2069
2078
  </button>
2079
+ <button class="action-btn analytics-btn" id="showAnalytics" onclick="showAnalyticsModal()">
2080
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2081
+ <line x1="18" y1="20" x2="18" y2="10"></line>
2082
+ <line x1="12" y1="20" x2="12" y2="4"></line>
2083
+ <line x1="6" y1="20" x2="6" y2="14"></line>
2084
+ </svg>
2085
+ <span>Analytics</span>
2086
+ </button>
2070
2087
  </div>
2071
2088
  </div>
2072
2089
  <div class="header-right">
@@ -2196,6 +2213,176 @@
2196
2213
  </div>
2197
2214
  </div>
2198
2215
 
2216
+ <!-- Analytics Modal -->
2217
+ <div class="modal-overlay" id="analyticsModal">
2218
+ <div class="modal" style="max-width: 900px; max-height: 90vh; overflow-y: auto;">
2219
+ <div class="modal-header">
2220
+ <h3 class="modal-title">Session Analytics (Beta)</h3>
2221
+ </div>
2222
+ <div style="padding: 20px;">
2223
+ <!-- Loading State -->
2224
+ <div id="analyticsLoading" style="text-align: center; padding: 40px;">
2225
+ <p style="color: var(--text-secondary);">Loading analytics data...</p>
2226
+ </div>
2227
+
2228
+ <!-- Analytics Content (hidden initially) -->
2229
+ <div id="analyticsContent" style="display: none;">
2230
+ <!-- Overview Stats - 3 columns -->
2231
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px;">
2232
+ <div style="background: rgba(139, 92, 246, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #8b5cf6;">
2233
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Total Messages</div>
2234
+ <div style="font-size: 24px; font-weight: 700; color: #8b5cf6;" id="analyticsMessageCount">-</div>
2235
+ </div>
2236
+ <div style="background: rgba(59, 130, 246, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #3b82f6;">
2237
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Total Tokens</div>
2238
+ <div style="font-size: 24px; font-weight: 700; color: #3b82f6;" id="analyticsTotalTokens">-</div>
2239
+ </div>
2240
+ <div style="background: rgba(16, 185, 129, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #10b981;">
2241
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Tool Calls</div>
2242
+ <div style="font-size: 24px; font-weight: 700; color: #10b981;" id="analyticsToolCalls">-</div>
2243
+ </div>
2244
+ <div style="background: rgba(234, 88, 12, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #ea580c;">
2245
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Tools Used</div>
2246
+ <div style="font-size: 24px; font-weight: 700; color: #ea580c;" id="analyticsUniqueTools">-</div>
2247
+ </div>
2248
+ <div style="background: rgba(251, 146, 60, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #fb923c;">
2249
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Models Used</div>
2250
+ <div style="font-size: 24px; font-weight: 700; color: #fb923c;" id="analyticsUniqueModels">-</div>
2251
+ </div>
2252
+ <div style="background: rgba(168, 85, 247, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #a855f7;">
2253
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Cache Efficiency</div>
2254
+ <div style="font-size: 24px; font-weight: 700; color: #a855f7;" id="analyticsCacheEfficiency">-</div>
2255
+ </div>
2256
+ </div>
2257
+
2258
+ <!-- Token Usage Breakdown -->
2259
+ <div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
2260
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2261
+ Token Usage Breakdown
2262
+ </h4>
2263
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
2264
+ <div>
2265
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Input Tokens</div>
2266
+ <div style="font-size: 18px; font-weight: 600; color: #3b82f6;" id="analyticsInputTokens">-</div>
2267
+ </div>
2268
+ <div>
2269
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Output Tokens</div>
2270
+ <div style="font-size: 18px; font-weight: 600; color: #8b5cf6;" id="analyticsOutputTokens">-</div>
2271
+ </div>
2272
+ <div>
2273
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Cache Creation</div>
2274
+ <div style="font-size: 18px; font-weight: 600; color: #fb923c;" id="analyticsCacheCreation">-</div>
2275
+ </div>
2276
+ <div>
2277
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Cache Reads</div>
2278
+ <div style="font-size: 18px; font-weight: 600; color: #10b981;" id="analyticsCacheReads">-</div>
2279
+ </div>
2280
+ </div>
2281
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.1);">
2282
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Cost Estimate (USD)</div>
2283
+ <div style="font-size: 16px; font-weight: 600; color: #10b981;" id="analyticsCostEstimate">$0.00</div>
2284
+ </div>
2285
+ </div>
2286
+
2287
+ <!-- Session Timeline -->
2288
+ <div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
2289
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2290
+ Session Timeline
2291
+ </h4>
2292
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
2293
+ <div>
2294
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Started</div>
2295
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsStartTime">-</div>
2296
+ </div>
2297
+ <div>
2298
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Last Activity</div>
2299
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsLastActivity">-</div>
2300
+ </div>
2301
+ <div>
2302
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Duration</div>
2303
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsDuration">-</div>
2304
+ </div>
2305
+ <div>
2306
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Status</div>
2307
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsStatus">-</div>
2308
+ </div>
2309
+ </div>
2310
+ </div>
2311
+
2312
+ <!-- Time Breakdown -->
2313
+ <div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
2314
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2315
+ Time Breakdown
2316
+ </h4>
2317
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
2318
+ <div>
2319
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Claude Executing</div>
2320
+ <div style="font-size: 18px; font-weight: 600; color: #3b82f6;" id="analyticsWaitTime">-</div>
2321
+ <div style="font-size: 11px; color: var(--text-secondary); margin-top: 2px;" id="analyticsWaitPercent">-</div>
2322
+ </div>
2323
+ <div>
2324
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">User Conversing</div>
2325
+ <div style="font-size: 18px; font-weight: 600; color: #10b981;" id="analyticsUserTime">-</div>
2326
+ <div style="font-size: 11px; color: var(--text-secondary); margin-top: 2px;" id="analyticsUserPercent">-</div>
2327
+ </div>
2328
+ </div>
2329
+ <div style="height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; display: flex;">
2330
+ <div id="analyticsWaitBar" style="background: linear-gradient(90deg, #3b82f6, #2563eb); transition: width 0.3s ease;"></div>
2331
+ <div id="analyticsUserBar" style="background: linear-gradient(90deg, #10b981, #059669); transition: width 0.3s ease;"></div>
2332
+ </div>
2333
+ </div>
2334
+
2335
+ <!-- Model Information -->
2336
+ <div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
2337
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2338
+ Model Information
2339
+ </h4>
2340
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
2341
+ <div>
2342
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Primary Model</div>
2343
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-family: monospace;" id="analyticsPrimaryModel">-</div>
2344
+ </div>
2345
+ <div>
2346
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Service Tier</div>
2347
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsServiceTier">-</div>
2348
+ </div>
2349
+ </div>
2350
+ <div id="analyticsAllModels" style="display: none; margin-top: 12px;">
2351
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">All Models Used</div>
2352
+ <div id="analyticsModelsList" style="display: flex; flex-wrap: wrap; gap: 6px;">
2353
+ <!-- Models will be populated here -->
2354
+ </div>
2355
+ </div>
2356
+ </div>
2357
+
2358
+ <!-- Tool Usage -->
2359
+ <div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
2360
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2361
+ Tool Usage
2362
+ </h4>
2363
+ <div id="analyticsToolsList" style="max-height: 200px; overflow-y: auto;">
2364
+ <!-- Tool usage will be populated here -->
2365
+ </div>
2366
+ </div>
2367
+
2368
+ <!-- Components Used -->
2369
+ <div id="analyticsComponentsSection" style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px; display: none;">
2370
+ <h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
2371
+ Components Used
2372
+ </h4>
2373
+ <div id="analyticsComponentsContent">
2374
+ <!-- Components will be populated here -->
2375
+ </div>
2376
+ </div>
2377
+ </div>
2378
+ </div>
2379
+
2380
+ <div class="modal-actions">
2381
+ <button class="modal-btn secondary" onclick="closeAnalyticsModal()">Close</button>
2382
+ </div>
2383
+ </div>
2384
+ </div>
2385
+
2199
2386
  <!-- Import WebSocket and Data Services -->
2200
2387
  <script src="services/WebSocketService.js"></script>
2201
2388
  <script src="services/DataService.js"></script>
@@ -2601,8 +2788,10 @@
2601
2788
 
2602
2789
  const resumeBtn = document.getElementById('resumeConversation');
2603
2790
  const downloadBtn = document.getElementById('downloadConversation');
2791
+ const analyticsBtn = document.getElementById('showAnalytics');
2604
2792
  resumeBtn.setAttribute('data-conversation-id', conversationId);
2605
2793
  downloadBtn.setAttribute('data-conversation-id', conversationId);
2794
+ analyticsBtn.setAttribute('data-conversation-id', conversationId);
2606
2795
 
2607
2796
  // Load messages (placeholder for now)
2608
2797
  this.loadChatMessages(conversationId);
@@ -4762,6 +4951,288 @@
4762
4951
  }
4763
4952
  }
4764
4953
 
4954
+ // Analytics Modal Functions
4955
+ async function showAnalyticsModal() {
4956
+ const analyticsBtn = document.getElementById('showAnalytics');
4957
+ const conversationId = analyticsBtn.getAttribute('data-conversation-id');
4958
+
4959
+ if (!conversationId) {
4960
+ console.error('No conversation ID found');
4961
+ return;
4962
+ }
4963
+
4964
+ console.log('📊 Opening analytics modal for conversation:', conversationId);
4965
+
4966
+ // Show analytics modal
4967
+ const analyticsModal = document.getElementById('analyticsModal');
4968
+ analyticsModal.classList.add('show');
4969
+
4970
+ // Reset to loading state
4971
+ document.getElementById('analyticsLoading').style.display = 'block';
4972
+ document.getElementById('analyticsContent').style.display = 'none';
4973
+
4974
+ // Close modal when clicking outside
4975
+ analyticsModal.addEventListener('click', (e) => {
4976
+ if (e.target === analyticsModal) {
4977
+ closeAnalyticsModal();
4978
+ }
4979
+ });
4980
+
4981
+ // Fetch analytics data
4982
+ try {
4983
+ const response = await fetch(`/api/conversations/${conversationId}/analytics`);
4984
+
4985
+ if (!response.ok) {
4986
+ throw new Error(`Failed to fetch analytics: ${response.statusText}`);
4987
+ }
4988
+
4989
+ const data = await response.json();
4990
+ console.log('✅ Analytics fetched:', data);
4991
+
4992
+ // Populate modal with analytics data
4993
+ populateAnalyticsModal(data.analytics);
4994
+
4995
+ // Hide loading, show content
4996
+ document.getElementById('analyticsLoading').style.display = 'none';
4997
+ document.getElementById('analyticsContent').style.display = 'block';
4998
+
4999
+ } catch (error) {
5000
+ console.error('❌ Failed to fetch analytics:', error);
5001
+ document.getElementById('analyticsLoading').innerHTML =
5002
+ `<p style="color: #ef4444;">Failed to load analytics: ${error.message}</p>`;
5003
+ }
5004
+ }
5005
+
5006
+ function closeAnalyticsModal() {
5007
+ const analyticsModal = document.getElementById('analyticsModal');
5008
+ analyticsModal.classList.remove('show');
5009
+ }
5010
+
5011
+ function populateAnalyticsModal(analytics) {
5012
+ console.log('📊 Populating analytics modal with data:', analytics);
5013
+
5014
+ // Overview stats - with safe fallbacks
5015
+ document.getElementById('analyticsMessageCount').textContent = (analytics.messageCount || 0).toLocaleString();
5016
+ document.getElementById('analyticsTotalTokens').textContent = (analytics.totalTokens || 0).toLocaleString();
5017
+ document.getElementById('analyticsToolCalls').textContent = (analytics.toolCalls || 0).toLocaleString();
5018
+ document.getElementById('analyticsCacheEfficiency').textContent = analytics.cacheEfficiency || '0%';
5019
+
5020
+ // New stats: unique tools and models
5021
+ const uniqueTools = analytics.toolUsage?.uniqueTools || 0;
5022
+ const uniqueModels = (analytics.modelInfo?.modelUsage && Array.isArray(analytics.modelInfo.modelUsage))
5023
+ ? analytics.modelInfo.modelUsage.length
5024
+ : 0;
5025
+
5026
+ console.log('📊 Unique tools:', uniqueTools, 'Unique models:', uniqueModels);
5027
+ document.getElementById('analyticsUniqueTools').textContent = uniqueTools;
5028
+ document.getElementById('analyticsUniqueModels').textContent = uniqueModels;
5029
+
5030
+ // Token breakdown
5031
+ document.getElementById('analyticsInputTokens').textContent = analytics.tokenUsage.inputTokens.toLocaleString();
5032
+ document.getElementById('analyticsOutputTokens').textContent = analytics.tokenUsage.outputTokens.toLocaleString();
5033
+ document.getElementById('analyticsCacheCreation').textContent = analytics.tokenUsage.cacheCreationTokens.toLocaleString();
5034
+ document.getElementById('analyticsCacheReads').textContent = analytics.tokenUsage.cacheReadTokens.toLocaleString();
5035
+
5036
+ // Cost estimate
5037
+ document.getElementById('analyticsCostEstimate').textContent = `$${analytics.costEstimate.total}`;
5038
+
5039
+ // Model info
5040
+ document.getElementById('analyticsPrimaryModel').textContent = analytics.modelInfo.primaryModel;
5041
+ document.getElementById('analyticsServiceTier').textContent = analytics.modelInfo.serviceTier;
5042
+
5043
+ // Show all models if multiple were used with usage percentages
5044
+ const allModelsSection = document.getElementById('analyticsAllModels');
5045
+ if (analytics.modelInfo.hasMultipleModels && analytics.modelInfo.modelUsage && analytics.modelInfo.modelUsage.length > 1) {
5046
+ const modelsListHTML = analytics.modelInfo.modelUsage
5047
+ .map(usage => `
5048
+ <span style="
5049
+ display: inline-flex;
5050
+ align-items: center;
5051
+ gap: 6px;
5052
+ padding: 4px 10px;
5053
+ background: rgba(139, 92, 246, 0.15);
5054
+ border: 1px solid rgba(139, 92, 246, 0.3);
5055
+ border-radius: 4px;
5056
+ font-size: 12px;
5057
+ font-family: monospace;
5058
+ color: var(--text-primary);
5059
+ ">
5060
+ <span>${usage.model}</span>
5061
+ <span style="
5062
+ padding: 2px 6px;
5063
+ background: rgba(139, 92, 246, 0.3);
5064
+ border-radius: 3px;
5065
+ font-size: 10px;
5066
+ font-weight: 600;
5067
+ color: #e9d5ff;
5068
+ ">${usage.count} msgs (${usage.percentage}%)</span>
5069
+ </span>
5070
+ `)
5071
+ .join('');
5072
+ document.getElementById('analyticsModelsList').innerHTML = modelsListHTML;
5073
+ allModelsSection.style.display = 'block';
5074
+ } else {
5075
+ allModelsSection.style.display = 'none';
5076
+ }
5077
+
5078
+ // Tool usage breakdown
5079
+ const toolsList = document.getElementById('analyticsToolsList');
5080
+ if (Object.keys(analytics.toolUsage.breakdown).length > 0) {
5081
+ const toolsHTML = Object.entries(analytics.toolUsage.breakdown)
5082
+ .sort((a, b) => b[1] - a[1]) // Sort by usage count
5083
+ .map(([tool, count]) => {
5084
+ const percentage = Math.round((count / analytics.toolUsage.totalCalls) * 100);
5085
+ return `
5086
+ <div style="margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 6px;">
5087
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
5088
+ <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);">${tool}</span>
5089
+ <span style="font-size: 12px; color: var(--text-secondary);">${count} calls (${percentage}%)</span>
5090
+ </div>
5091
+ <div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
5092
+ <div style="height: 100%; width: ${percentage}%; background: linear-gradient(90deg, #10b981, #059669); transition: width 0.3s ease;"></div>
5093
+ </div>
5094
+ </div>
5095
+ `;
5096
+ })
5097
+ .join('');
5098
+ toolsList.innerHTML = toolsHTML;
5099
+ } else {
5100
+ toolsList.innerHTML = '<p style="color: var(--text-secondary); font-size: 13px; text-align: center; padding: 20px;">No tool usage detected in this session</p>';
5101
+ }
5102
+
5103
+ // Timeline
5104
+ const formatDate = (isoString) => {
5105
+ if (!isoString) return 'N/A';
5106
+ const date = new Date(isoString);
5107
+ return date.toLocaleString('en-US', {
5108
+ month: 'short',
5109
+ day: 'numeric',
5110
+ hour: '2-digit',
5111
+ minute: '2-digit'
5112
+ });
5113
+ };
5114
+
5115
+ document.getElementById('analyticsStartTime').textContent = formatDate(analytics.timeline.startTime);
5116
+ document.getElementById('analyticsLastActivity').textContent = formatDate(analytics.timeline.endTime);
5117
+ document.getElementById('analyticsDuration').textContent = analytics.timeline.duration;
5118
+ document.getElementById('analyticsStatus').textContent = analytics.timeline.status.charAt(0).toUpperCase() + analytics.timeline.status.slice(1);
5119
+
5120
+ // Time breakdown
5121
+ if (analytics.timeBreakdown && analytics.timeBreakdown.totalWaitTime) {
5122
+ document.getElementById('analyticsWaitTime').textContent = analytics.timeBreakdown.totalWaitTime || '0s';
5123
+ document.getElementById('analyticsUserTime').textContent = analytics.timeBreakdown.totalUserTime || '0s';
5124
+ document.getElementById('analyticsWaitPercent').textContent = `${analytics.timeBreakdown.waitTimePercent || 0}% of iteration time`;
5125
+ document.getElementById('analyticsUserPercent').textContent = `${analytics.timeBreakdown.userTimePercent || 0}% of iteration time`;
5126
+
5127
+ // Update progress bars
5128
+ document.getElementById('analyticsWaitBar').style.width = `${analytics.timeBreakdown.waitTimePercent || 0}%`;
5129
+ document.getElementById('analyticsUserBar').style.width = `${analytics.timeBreakdown.userTimePercent || 0}%`;
5130
+ } else {
5131
+ // No timing data available
5132
+ document.getElementById('analyticsWaitTime').textContent = 'N/A';
5133
+ document.getElementById('analyticsUserTime').textContent = 'N/A';
5134
+ document.getElementById('analyticsWaitPercent').textContent = 'No data';
5135
+ document.getElementById('analyticsUserPercent').textContent = 'No data';
5136
+ document.getElementById('analyticsWaitBar').style.width = '0%';
5137
+ document.getElementById('analyticsUserBar').style.width = '0%';
5138
+ }
5139
+
5140
+ // Components used (agents, commands, skills)
5141
+ if (analytics.componentsUsed) {
5142
+ const hasComponents = analytics.componentsUsed.totalAgents > 0 ||
5143
+ analytics.componentsUsed.totalCommands > 0 ||
5144
+ analytics.componentsUsed.totalSkills > 0;
5145
+
5146
+ if (hasComponents) {
5147
+ let componentsHTML = '';
5148
+
5149
+ // Agents
5150
+ if (analytics.componentsUsed.agents.length > 0) {
5151
+ componentsHTML += '<div style="margin-bottom: 12px;">';
5152
+ componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Agents</div>';
5153
+ componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
5154
+ analytics.componentsUsed.agents.forEach(agent => {
5155
+ componentsHTML += `
5156
+ <span style="
5157
+ display: inline-flex;
5158
+ align-items: center;
5159
+ gap: 4px;
5160
+ padding: 4px 10px;
5161
+ background: rgba(139, 92, 246, 0.15);
5162
+ border: 1px solid rgba(139, 92, 246, 0.3);
5163
+ border-radius: 4px;
5164
+ font-size: 12px;
5165
+ color: var(--text-primary);
5166
+ ">
5167
+ <span>${agent.type}</span>
5168
+ <span style="color: var(--text-secondary); font-size: 11px;">×${agent.count}</span>
5169
+ </span>
5170
+ `;
5171
+ });
5172
+ componentsHTML += '</div></div>';
5173
+ }
5174
+
5175
+ // Slash Commands
5176
+ if (analytics.componentsUsed.slashCommands.length > 0) {
5177
+ componentsHTML += '<div style="margin-bottom: 12px;">';
5178
+ componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Slash Commands</div>';
5179
+ componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
5180
+ analytics.componentsUsed.slashCommands.forEach(cmd => {
5181
+ componentsHTML += `
5182
+ <span style="
5183
+ display: inline-flex;
5184
+ align-items: center;
5185
+ gap: 4px;
5186
+ padding: 4px 10px;
5187
+ background: rgba(59, 130, 246, 0.15);
5188
+ border: 1px solid rgba(59, 130, 246, 0.3);
5189
+ border-radius: 4px;
5190
+ font-size: 12px;
5191
+ font-family: monospace;
5192
+ color: var(--text-primary);
5193
+ ">
5194
+ <span>${cmd.name}</span>
5195
+ <span style="color: var(--text-secondary); font-size: 11px;">×${cmd.count}</span>
5196
+ </span>
5197
+ `;
5198
+ });
5199
+ componentsHTML += '</div></div>';
5200
+ }
5201
+
5202
+ // Skills
5203
+ if (analytics.componentsUsed.skills.length > 0) {
5204
+ componentsHTML += '<div>';
5205
+ componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Skills</div>';
5206
+ componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
5207
+ analytics.componentsUsed.skills.forEach(skill => {
5208
+ componentsHTML += `
5209
+ <span style="
5210
+ display: inline-flex;
5211
+ align-items: center;
5212
+ gap: 4px;
5213
+ padding: 4px 10px;
5214
+ background: rgba(16, 185, 129, 0.15);
5215
+ border: 1px solid rgba(16, 185, 129, 0.3);
5216
+ border-radius: 4px;
5217
+ font-size: 12px;
5218
+ color: var(--text-primary);
5219
+ ">
5220
+ <span>${skill.name}</span>
5221
+ <span style="color: var(--text-secondary); font-size: 11px;">×${skill.count}</span>
5222
+ </span>
5223
+ `;
5224
+ });
5225
+ componentsHTML += '</div></div>';
5226
+ }
5227
+
5228
+ document.getElementById('analyticsComponentsContent').innerHTML = componentsHTML;
5229
+ document.getElementById('analyticsComponentsSection').style.display = 'block';
5230
+ } else {
5231
+ document.getElementById('analyticsComponentsSection').style.display = 'none';
5232
+ }
5233
+ }
5234
+ }
5235
+
4765
5236
  // Initialize the app
4766
5237
  document.addEventListener('DOMContentLoaded', () => {
4767
5238
  new ChatsMobileApp();
@@ -9,6 +9,7 @@ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
9
9
  const StateCalculator = require('./analytics/core/StateCalculator');
10
10
  const FileWatcher = require('./analytics/core/FileWatcher');
11
11
  const DataCache = require('./analytics/data/DataCache');
12
+ const AgentAnalyzer = require('./analytics/core/AgentAnalyzer');
12
13
  const WebSocketServer = require('./analytics/notifications/WebSocketServer');
13
14
  const SessionSharing = require('./session-sharing');
14
15
 
@@ -512,6 +513,289 @@ class ChatsMobile {
512
513
  }
513
514
  });
514
515
 
516
+ // API to get detailed analytics for a conversation
517
+ this.app.get('/api/conversations/:id/analytics', async (req, res) => {
518
+ try {
519
+ const conversationId = req.params.id;
520
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
521
+
522
+ if (!conversation) {
523
+ return res.status(404).json({ error: 'Conversation not found' });
524
+ }
525
+
526
+ console.log(chalk.cyan(`📊 Fetching analytics for conversation ${conversationId}...`));
527
+
528
+ // Get parsed messages for this conversation
529
+ const messages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
530
+
531
+ // Calculate session duration and timing breakdown
532
+ const startTime = messages.length > 0 ? new Date(messages[0].timestamp) : null;
533
+ const endTime = messages.length > 0 ? new Date(messages[messages.length - 1].timestamp) : null;
534
+ const durationMs = startTime && endTime ? endTime - startTime : 0;
535
+ const durationHours = Math.floor(durationMs / (1000 * 60 * 60));
536
+ const durationMinutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
537
+
538
+ // Calculate time conversing vs executing (time between messages)
539
+ let totalWaitTime = 0; // Time waiting for Claude (thinking + executing)
540
+ let totalUserTime = 0; // Time user takes to respond
541
+
542
+ // Find user and assistant messages only (ignore tool results and other message types)
543
+ const conversationMessages = messages.filter(msg => msg.role === 'user' || msg.role === 'assistant');
544
+
545
+ let lastUserTime = null;
546
+ let lastAssistantTime = null;
547
+
548
+ conversationMessages.forEach(msg => {
549
+ if (msg.role === 'user') {
550
+ // If there was a previous assistant message, calculate user thinking time
551
+ if (lastAssistantTime) {
552
+ const thinkingTime = new Date(msg.timestamp) - lastAssistantTime;
553
+ // Only count gaps less than 1 hour to avoid counting long breaks
554
+ if (thinkingTime > 0 && thinkingTime < 60 * 60 * 1000) {
555
+ totalUserTime += thinkingTime;
556
+ }
557
+ }
558
+ lastUserTime = new Date(msg.timestamp);
559
+ } else if (msg.role === 'assistant') {
560
+ // If there was a previous user message, calculate Claude execution time
561
+ if (lastUserTime) {
562
+ const executionTime = new Date(msg.timestamp) - lastUserTime;
563
+ // Only count gaps less than 10 minutes (typical execution time)
564
+ if (executionTime > 0 && executionTime < 10 * 60 * 1000) {
565
+ totalWaitTime += executionTime;
566
+ }
567
+ }
568
+ lastAssistantTime = new Date(msg.timestamp);
569
+ }
570
+ });
571
+
572
+ const totalIterationTime = totalWaitTime + totalUserTime;
573
+ const waitTimePercent = totalIterationTime > 0 ? Math.round((totalWaitTime / totalIterationTime) * 100) : 0;
574
+ const userTimePercent = totalIterationTime > 0 ? Math.round((totalUserTime / totalIterationTime) * 100) : 0;
575
+
576
+ // Calculate cache efficiency
577
+ const cacheTotal = (conversation.tokenUsage?.cacheCreationTokens || 0) + (conversation.tokenUsage?.cacheReadTokens || 0);
578
+ const cacheEfficiency = cacheTotal > 0
579
+ ? Math.round((conversation.tokenUsage?.cacheReadTokens || 0) / cacheTotal * 100)
580
+ : 0;
581
+
582
+ // Estimate cost (approximate Claude API pricing)
583
+ // Sonnet 4.5: $3/1M input, $15/1M output
584
+ // Cache write: $3.75/1M, Cache read: $0.30/1M
585
+ const inputCost = (conversation.tokenUsage?.inputTokens || 0) / 1000000 * 3;
586
+ const outputCost = (conversation.tokenUsage?.outputTokens || 0) / 1000000 * 15;
587
+ const cacheWriteCost = (conversation.tokenUsage?.cacheCreationTokens || 0) / 1000000 * 3.75;
588
+ const cacheReadCost = (conversation.tokenUsage?.cacheReadTokens || 0) / 1000000 * 0.30;
589
+ const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
590
+
591
+ // Detect agents, hooks, and components used
592
+ const agentAnalyzer = new AgentAnalyzer();
593
+ const componentsUsed = {
594
+ agents: [],
595
+ slashCommands: [],
596
+ skills: []
597
+ };
598
+
599
+ messages.forEach(message => {
600
+ const messageContent = message.content;
601
+ const messageRole = message.role;
602
+
603
+ if (messageRole === 'assistant' && messageContent && Array.isArray(messageContent)) {
604
+ messageContent.forEach(content => {
605
+ // Detect Task tool with subagent_type (agents)
606
+ if (content.type === 'tool_use' && content.name === 'Task' && content.input?.subagent_type) {
607
+ const agentType = content.input.subagent_type;
608
+ if (!componentsUsed.agents.find(a => a.type === agentType)) {
609
+ componentsUsed.agents.push({
610
+ type: agentType,
611
+ count: 1
612
+ });
613
+ } else {
614
+ componentsUsed.agents.find(a => a.type === agentType).count++;
615
+ }
616
+ }
617
+
618
+ // Detect SlashCommand tool (commands)
619
+ if (content.type === 'tool_use' && content.name === 'SlashCommand' && content.input?.command) {
620
+ const command = content.input.command;
621
+ if (!componentsUsed.slashCommands.find(c => c.name === command)) {
622
+ componentsUsed.slashCommands.push({
623
+ name: command,
624
+ count: 1
625
+ });
626
+ } else {
627
+ componentsUsed.slashCommands.find(c => c.name === command).count++;
628
+ }
629
+ }
630
+
631
+ // Detect Skill tool (skills)
632
+ if (content.type === 'tool_use' && content.name === 'Skill' && content.input?.command) {
633
+ const skill = content.input.command;
634
+ if (!componentsUsed.skills.find(s => s.name === skill)) {
635
+ componentsUsed.skills.push({
636
+ name: skill,
637
+ count: 1
638
+ });
639
+ } else {
640
+ componentsUsed.skills.find(s => s.name === skill).count++;
641
+ }
642
+ }
643
+ });
644
+ }
645
+ });
646
+
647
+ // Format time durations
648
+ const formatDuration = (ms) => {
649
+ const hours = Math.floor(ms / (1000 * 60 * 60));
650
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
651
+ const seconds = Math.floor((ms % (1000 * 60)) / 1000);
652
+
653
+ if (hours > 0) return `${hours}h ${minutes}m`;
654
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
655
+ return `${seconds}s`;
656
+ };
657
+
658
+ // Generate optimization tips based on analytics
659
+ const optimizationTips = [];
660
+
661
+ if (cacheEfficiency < 20 && cacheTotal > 0) {
662
+ optimizationTips.push('• Low cache efficiency detected. Consider restructuring prompts to maximize cache reuse.');
663
+ }
664
+ if (conversation.toolUsage?.totalToolCalls > 50) {
665
+ optimizationTips.push('• High tool usage detected. Review if all tool calls are necessary.');
666
+ }
667
+ if (conversation.tokenUsage?.outputTokens > conversation.tokenUsage?.inputTokens * 2) {
668
+ optimizationTips.push('• Output tokens significantly exceed input. Consider more concise prompts.');
669
+ }
670
+ if (messages.length > 100) {
671
+ optimizationTips.push('• Long conversation detected. Consider starting fresh sessions for new topics to optimize context.');
672
+ }
673
+ if (conversation.modelInfo?.hasMultipleModels) {
674
+ optimizationTips.push('• Multiple models used in this session. Stick to one model for consistency.');
675
+ }
676
+ if (waitTimePercent > 70) {
677
+ optimizationTips.push('• High execution time detected. Consider breaking down complex tasks or optimizing tool usage.');
678
+ }
679
+ if (optimizationTips.length === 0) {
680
+ optimizationTips.push('• Great! Your conversation shows efficient usage patterns.');
681
+ }
682
+
683
+ // Prepare detailed analytics response
684
+ const analytics = {
685
+ // Overview
686
+ messageCount: messages.length,
687
+ totalTokens: conversation.tokenUsage?.total || 0,
688
+ toolCalls: conversation.toolUsage?.totalToolCalls || 0,
689
+ cacheEfficiency: `${cacheEfficiency}%`,
690
+
691
+ // Token breakdown
692
+ tokenUsage: {
693
+ inputTokens: conversation.tokenUsage?.inputTokens || 0,
694
+ outputTokens: conversation.tokenUsage?.outputTokens || 0,
695
+ cacheCreationTokens: conversation.tokenUsage?.cacheCreationTokens || 0,
696
+ cacheReadTokens: conversation.tokenUsage?.cacheReadTokens || 0,
697
+ total: conversation.tokenUsage?.total || 0
698
+ },
699
+
700
+ // Cost estimate
701
+ costEstimate: {
702
+ total: totalCost.toFixed(4),
703
+ breakdown: {
704
+ input: inputCost.toFixed(4),
705
+ output: outputCost.toFixed(4),
706
+ cacheWrite: cacheWriteCost.toFixed(4),
707
+ cacheRead: cacheReadCost.toFixed(4)
708
+ }
709
+ },
710
+
711
+ // Model info with usage percentages
712
+ modelInfo: {
713
+ primaryModel: conversation.modelInfo?.primaryModel || 'Unknown',
714
+ serviceTier: conversation.modelInfo?.currentServiceTier || 'Unknown',
715
+ hasMultipleModels: conversation.modelInfo?.hasMultipleModels || false,
716
+ allModels: conversation.modelInfo?.models || [],
717
+ modelUsage: (() => {
718
+ // Calculate model usage percentages
719
+ const modelCounts = {};
720
+ let totalMessages = 0;
721
+
722
+ messages.forEach(msg => {
723
+ if (msg.model && msg.model !== '<synthetic>') {
724
+ modelCounts[msg.model] = (modelCounts[msg.model] || 0) + 1;
725
+ totalMessages++;
726
+ }
727
+ });
728
+
729
+ return Object.entries(modelCounts).map(([model, count]) => ({
730
+ model,
731
+ count,
732
+ percentage: totalMessages > 0 ? ((count / totalMessages) * 100).toFixed(1) : '0.0'
733
+ })).sort((a, b) => b.count - a.count);
734
+ })()
735
+ },
736
+
737
+ // Tool usage
738
+ toolUsage: {
739
+ totalCalls: conversation.toolUsage?.totalToolCalls || 0,
740
+ uniqueTools: conversation.toolUsage?.uniqueTools || 0,
741
+ breakdown: conversation.toolUsage?.toolStats || {},
742
+ timeline: conversation.toolUsage?.toolTimeline || []
743
+ },
744
+
745
+ // Session timeline
746
+ timeline: {
747
+ startTime: startTime ? startTime.toISOString() : null,
748
+ endTime: endTime ? endTime.toISOString() : null,
749
+ duration: durationHours > 0
750
+ ? `${durationHours}h ${durationMinutes}m`
751
+ : `${durationMinutes}m`,
752
+ durationMs: durationMs,
753
+ status: conversation.status || 'unknown'
754
+ },
755
+
756
+ // Time breakdown (conversing vs executing)
757
+ timeBreakdown: {
758
+ totalWaitTime: formatDuration(totalWaitTime),
759
+ totalUserTime: formatDuration(totalUserTime),
760
+ waitTimePercent: waitTimePercent,
761
+ userTimePercent: userTimePercent,
762
+ waitTimeMs: totalWaitTime,
763
+ userTimeMs: totalUserTime,
764
+ totalIterationTime: formatDuration(totalIterationTime)
765
+ },
766
+
767
+ // Components used (agents, commands, skills)
768
+ componentsUsed: {
769
+ agents: componentsUsed.agents.sort((a, b) => b.count - a.count),
770
+ slashCommands: componentsUsed.slashCommands.sort((a, b) => b.count - a.count),
771
+ skills: componentsUsed.skills.sort((a, b) => b.count - a.count),
772
+ totalAgents: componentsUsed.agents.length,
773
+ totalCommands: componentsUsed.slashCommands.length,
774
+ totalSkills: componentsUsed.skills.length
775
+ },
776
+
777
+ // Optimization tips
778
+ optimizationTips: optimizationTips,
779
+
780
+ // Metadata
781
+ conversationId: conversationId,
782
+ project: conversation.project || 'Unknown',
783
+ timestamp: new Date().toISOString()
784
+ };
785
+
786
+ res.json({
787
+ success: true,
788
+ analytics: analytics
789
+ });
790
+ } catch (error) {
791
+ console.error('Error fetching conversation analytics:', error);
792
+ res.status(500).json({
793
+ error: 'Failed to fetch analytics',
794
+ message: error.message
795
+ });
796
+ }
797
+ });
798
+
515
799
  // Serve the mobile chats page as default
516
800
  this.app.get('/', (req, res) => {
517
801
  res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
@@ -903,6 +1187,12 @@ class ChatsMobile {
903
1187
  * Stop the server
904
1188
  */
905
1189
  async stop() {
1190
+ // Prevent multiple stop calls
1191
+ if (this.isStopped) {
1192
+ return;
1193
+ }
1194
+ this.isStopped = true;
1195
+
906
1196
  if (this.cloudflaredProcess) {
907
1197
  try {
908
1198
  this.cloudflaredProcess.kill('SIGTERM');
@@ -911,26 +1201,28 @@ class ChatsMobile {
911
1201
  this.log('warn', chalk.yellow('⚠️ Error stopping Cloudflare Tunnel:', error.message));
912
1202
  }
913
1203
  }
914
-
1204
+
915
1205
  if (this.webSocketServer) {
916
1206
  try {
1207
+ console.log(chalk.gray('🔌 Closing WebSocket server...'));
917
1208
  await this.webSocketServer.close();
918
- this.log('info', chalk.gray('🌐 WebSocket server stopped'));
1209
+ console.log(chalk.green(' WebSocket server closed'));
919
1210
  } catch (error) {
920
1211
  this.log('warn', chalk.yellow('⚠️ Error stopping WebSocket server:', error.message));
921
1212
  }
922
1213
  }
923
-
1214
+
924
1215
  if (this.httpServer) {
925
1216
  await new Promise((resolve) => {
926
1217
  this.httpServer.close(resolve);
927
1218
  });
928
1219
  }
929
-
1220
+
930
1221
  if (this.fileWatcher) {
1222
+ console.log(chalk.gray('🛑 Stopping file watchers...'));
931
1223
  await this.fileWatcher.stop();
932
1224
  }
933
-
1225
+
934
1226
  console.log(chalk.gray('🛑 Chats Mobile server stopped'));
935
1227
  }
936
1228
  }
@@ -961,14 +1253,35 @@ async function startChatsMobile(options = {}) {
961
1253
  }
962
1254
 
963
1255
  console.log(chalk.gray('Press Ctrl+C to stop'));
964
-
965
- // Handle graceful shutdown
966
- process.on('SIGINT', async () => {
1256
+
1257
+ // Handle graceful shutdown - remove existing listeners first to prevent duplicates
1258
+ const shutdownHandler = async () => {
1259
+ if (chatsMobile.isShuttingDown) return; // Prevent multiple shutdown attempts
1260
+ chatsMobile.isShuttingDown = true;
1261
+
967
1262
  console.log(chalk.yellow('\n🛑 Shutting down...'));
968
- await chatsMobile.stop();
969
- process.exit(0);
970
- });
971
-
1263
+
1264
+ // Remove this specific handler to prevent it from being called again
1265
+ process.removeListener('SIGINT', shutdownHandler);
1266
+ process.removeListener('SIGTERM', shutdownHandler);
1267
+
1268
+ try {
1269
+ await chatsMobile.stop();
1270
+ process.exit(0);
1271
+ } catch (error) {
1272
+ console.error(chalk.red('❌ Error during shutdown:'), error);
1273
+ process.exit(1);
1274
+ }
1275
+ };
1276
+
1277
+ // Remove any existing SIGINT/SIGTERM listeners to prevent duplicates
1278
+ process.removeAllListeners('SIGINT');
1279
+ process.removeAllListeners('SIGTERM');
1280
+
1281
+ // Add the new handler
1282
+ process.on('SIGINT', shutdownHandler);
1283
+ process.on('SIGTERM', shutdownHandler);
1284
+
972
1285
  } catch (error) {
973
1286
  console.error(chalk.red('❌ Failed to start Chats Mobile:'), error);
974
1287
  process.exit(1);