claude-code-templates 1.10.0 → 1.11.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.
Files changed (35) hide show
  1. package/README.md +6 -0
  2. package/bin/create-claude-config.js +1 -0
  3. package/package.json +2 -2
  4. package/src/analytics/core/ConversationAnalyzer.js +94 -20
  5. package/src/analytics/core/FileWatcher.js +146 -11
  6. package/src/analytics/data/DataCache.js +124 -19
  7. package/src/analytics/notifications/NotificationManager.js +37 -0
  8. package/src/analytics/notifications/WebSocketServer.js +1 -1
  9. package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
  10. package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
  11. package/src/analytics-web/components/AgentsPage.js +2535 -0
  12. package/src/analytics-web/components/App.js +430 -0
  13. package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
  14. package/src/analytics-web/components/DashboardPage.js +1527 -0
  15. package/src/analytics-web/components/Sidebar.js +197 -0
  16. package/src/analytics-web/components/ToolDisplay.js +539 -0
  17. package/src/analytics-web/index.html +3275 -1792
  18. package/src/analytics-web/services/DataService.js +89 -16
  19. package/src/analytics-web/services/StateService.js +9 -0
  20. package/src/analytics-web/services/WebSocketService.js +17 -5
  21. package/src/analytics.js +323 -35
  22. package/src/console-bridge.js +610 -0
  23. package/src/file-operations.js +143 -23
  24. package/src/hook-scanner.js +21 -1
  25. package/src/index.js +24 -1
  26. package/src/templates.js +28 -0
  27. package/src/test-console-bridge.js +67 -0
  28. package/src/utils.js +46 -0
  29. package/templates/ruby/.claude/commands/model.md +360 -0
  30. package/templates/ruby/.claude/commands/test.md +480 -0
  31. package/templates/ruby/.claude/settings.json +146 -0
  32. package/templates/ruby/.mcp.json +83 -0
  33. package/templates/ruby/CLAUDE.md +284 -0
  34. package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +490 -0
  35. package/templates/ruby/examples/rails-app/CLAUDE.md +376 -0
package/README.md CHANGED
@@ -76,6 +76,10 @@ npx claude-code-templates
76
76
  ```bash
77
77
  # Launch real-time analytics dashboard
78
78
  npx claude-code-templates --analytics
79
+
80
+ # Launch chats/conversations dashboard (opens directly to conversations)
81
+ npx claude-code-templates --chats
82
+ npx claude-code-templates --agents
79
83
  ```
80
84
 
81
85
  ### Health Check
@@ -128,6 +132,8 @@ npx create-claude-config # Create-style command
128
132
  | `-y, --yes` | Skip prompts and use defaults | `--yes` |
129
133
  | `--dry-run` | Show what would be installed | `--dry-run` |
130
134
  | `--analytics` | Launch real-time analytics dashboard | `--analytics` |
135
+ | `--chats` | Launch chats/conversations dashboard | `--chats` |
136
+ | `--agents` | Launch agents dashboard (alias for chats) | `--agents` |
131
137
  | `--health-check` | Run comprehensive system validation | `--health-check` |
132
138
  | `--health` | Run system health check (alias) | `--health` |
133
139
  | `--check` | Run system validation (alias) | `--check` |
@@ -45,6 +45,7 @@ program
45
45
  .option('--hook-stats, --hooks-stats', 'analyze existing automation hooks and offer optimization')
46
46
  .option('--mcp-stats, --mcps-stats', 'analyze existing MCP server configurations and offer optimization')
47
47
  .option('--analytics', 'launch real-time Claude Code analytics dashboard')
48
+ .option('--chats, --agents', 'launch Claude Code chats/agents dashboard (opens directly to conversations)')
48
49
  .option('--health-check, --health, --check, --verify', 'run comprehensive health check to verify Claude Code setup')
49
50
  .action(async (options) => {
50
51
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
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": {
@@ -31,7 +31,7 @@
31
31
  "dev:link": "npm link",
32
32
  "dev:unlink": "npm unlink -g claude-code-templates",
33
33
  "pretest:commands": "npm run dev:link",
34
- "prepublishOnly": "echo 'Skipping tests for Health Check release'",
34
+ "prepublishOnly": "echo 'Skipping tests for emergency publish'",
35
35
  "analytics:start": "node src/analytics.js",
36
36
  "analytics:test": "npm run test:analytics"
37
37
  },
@@ -93,7 +93,7 @@ class ConversationAnalyzer {
93
93
  };
94
94
 
95
95
  const jsonlFiles = await findJsonlFiles(this.claudeDir);
96
- console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
96
+ // Loading conversation files quietly for better UX
97
97
 
98
98
  for (const filePath of jsonlFiles) {
99
99
  const stats = await this.getFileStats(filePath);
@@ -216,26 +216,96 @@ class ConversationAnalyzer {
216
216
  return await this.dataCache.getParsedConversation(filepath);
217
217
  }
218
218
 
219
- // Fallback to direct parsing
219
+ // Fallback to direct parsing with tool correlation
220
220
  const content = await fs.readFile(filepath, 'utf8');
221
- return content.trim().split('\n')
222
- .filter(line => line.trim())
223
- .map(line => {
224
- try {
225
- const item = JSON.parse(line);
226
- if (item.message && item.message.role) {
227
- return {
228
- role: item.message.role,
229
- timestamp: new Date(item.timestamp),
230
- content: item.message.content,
231
- model: item.message.model || null,
232
- usage: item.message.usage || null,
233
- };
221
+ const lines = content.trim().split('\n').filter(line => line.trim());
222
+
223
+ return this.parseAndCorrelateToolMessages(lines);
224
+ }
225
+
226
+ /**
227
+ * Parse JSONL lines and correlate tool_use with tool_result
228
+ * @param {Array} lines - JSONL lines
229
+ * @returns {Array} Parsed and correlated messages
230
+ */
231
+ parseAndCorrelateToolMessages(lines) {
232
+ const entries = [];
233
+ const toolUseMap = new Map();
234
+
235
+ // First pass: parse all entries and map tool_use entries
236
+ for (const line of lines) {
237
+ try {
238
+ const item = JSON.parse(line);
239
+ if (item.message && (item.type === 'assistant' || item.type === 'user')) {
240
+ entries.push(item);
241
+
242
+ // Track tool_use entries by their ID
243
+ if (item.type === 'assistant' && item.message.content) {
244
+ const toolUseBlock = Array.isArray(item.message.content)
245
+ ? item.message.content.find(c => c.type === 'tool_use')
246
+ : (item.message.content.type === 'tool_use' ? item.message.content : null);
247
+
248
+ if (toolUseBlock && toolUseBlock.id) {
249
+ toolUseMap.set(toolUseBlock.id, item);
250
+ }
234
251
  }
235
- } catch {}
236
- return null;
237
- })
238
- .filter(Boolean);
252
+ }
253
+ } catch (error) {
254
+ // Skip invalid JSONL lines
255
+ }
256
+ }
257
+
258
+ // Second pass: correlate tool_result with tool_use and filter out standalone tool_result entries
259
+ const processedMessages = [];
260
+
261
+ for (const item of entries) {
262
+ if (item.type === 'user' && item.message.content) {
263
+ // Check if this is a tool_result entry
264
+ const toolResultBlock = Array.isArray(item.message.content)
265
+ ? item.message.content.find(c => c.type === 'tool_result')
266
+ : (item.message.content.type === 'tool_result' ? item.message.content : null);
267
+
268
+ if (toolResultBlock && toolResultBlock.tool_use_id) {
269
+ // This is a tool_result - attach it to the corresponding tool_use
270
+ // console.log(`🔍 ConversationAnalyzer: Found tool_result for ${toolResultBlock.tool_use_id}, content: "${toolResultBlock.content}"`);
271
+ const toolUseEntry = toolUseMap.get(toolResultBlock.tool_use_id);
272
+ // console.log(`🔍 ConversationAnalyzer: toolUseEntry found: ${!!toolUseEntry}`);
273
+ if (toolUseEntry) {
274
+ // Attach tool result to the tool use entry
275
+ if (!toolUseEntry.toolResults) {
276
+ toolUseEntry.toolResults = [];
277
+ }
278
+ toolUseEntry.toolResults.push(toolResultBlock);
279
+ // console.log(`✅ ConversationAnalyzer: Attached tool result to ${toolResultBlock.tool_use_id}, content length: ${toolResultBlock.content?.length || 0}`);
280
+ // Don't add this tool_result as a separate message
281
+ continue;
282
+ } else {
283
+ // console.log(`❌ ConversationAnalyzer: No tool_use found for ${toolResultBlock.tool_use_id}`);
284
+ }
285
+ }
286
+ }
287
+
288
+ // Convert to our standard format
289
+ if (item.toolResults) {
290
+ // console.log(`ConversationAnalyzer: Processing item with ${item.toolResults.length} tool results`);
291
+ }
292
+ const parsed = {
293
+ id: item.message.id || item.uuid || null,
294
+ role: item.message.role || (item.type === 'assistant' ? 'assistant' : 'user'),
295
+ timestamp: new Date(item.timestamp),
296
+ content: item.message.content,
297
+ model: item.message.model || null,
298
+ usage: item.message.usage || null,
299
+ toolResults: item.toolResults || null, // Include attached tool results
300
+ isCompactSummary: item.isCompactSummary || false, // Preserve compact summary flag
301
+ uuid: item.uuid || null, // Include UUID for message identification
302
+ type: item.type || null // Include type field
303
+ };
304
+
305
+ processedMessages.push(parsed);
306
+ }
307
+
308
+ return processedMessages;
239
309
  }
240
310
 
241
311
  /**
@@ -576,7 +646,8 @@ class ConversationAnalyzer {
576
646
  const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
577
647
 
578
648
  // Calculate real Claude sessions (5-hour periods)
579
- const claudeSessions = await this.calculateClaudeSessions(conversations);
649
+ const claudeSessionsResult = await this.calculateClaudeSessions(conversations);
650
+ const claudeSessions = claudeSessionsResult?.total || 0;
580
651
 
581
652
  return {
582
653
  totalConversations,
@@ -585,8 +656,11 @@ class ConversationAnalyzer {
585
656
  activeProjects,
586
657
  avgTokensPerConversation,
587
658
  totalFileSize: this.formatBytes(totalFileSize),
659
+ dataSize: this.formatBytes(totalFileSize), // Alias for original dashboard compatibility
588
660
  lastActivity: conversations.length > 0 ? conversations[0].lastModified : null,
589
661
  claudeSessions,
662
+ claudeSessionsDetail: claudeSessions > 0 ? `${claudeSessions} session${claudeSessions > 1 ? 's' : ''}` : 'no sessions',
663
+ claudeSessionsFullData: claudeSessionsResult, // Keep full session data for detailed analysis
590
664
  };
591
665
  }
592
666
 
@@ -11,6 +11,8 @@ class FileWatcher {
11
11
  this.watchers = [];
12
12
  this.intervals = [];
13
13
  this.isActive = false;
14
+ this.fileActivity = new Map(); // Track file activity for typing detection
15
+ this.typingTimeout = new Map(); // Track typing timeouts
14
16
  }
15
17
 
16
18
  /**
@@ -20,13 +22,14 @@ class FileWatcher {
20
22
  * @param {Function} processRefreshCallback - Callback to refresh process data
21
23
  * @param {Object} dataCache - DataCache instance for invalidation
22
24
  */
23
- setupFileWatchers(claudeDir, dataRefreshCallback, processRefreshCallback, dataCache = null) {
25
+ setupFileWatchers(claudeDir, dataRefreshCallback, processRefreshCallback, dataCache = null, conversationChangeCallback = null) {
24
26
  console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
25
27
 
26
28
  this.claudeDir = claudeDir;
27
29
  this.dataRefreshCallback = dataRefreshCallback;
28
30
  this.processRefreshCallback = processRefreshCallback;
29
31
  this.dataCache = dataCache;
32
+ this.conversationChangeCallback = conversationChangeCallback;
30
33
 
31
34
  this.setupConversationWatcher();
32
35
  this.setupProjectWatcher();
@@ -47,21 +50,28 @@ class FileWatcher {
47
50
  });
48
51
 
49
52
  conversationWatcher.on('change', async (filePath) => {
50
- console.log(chalk.yellow('🔄 Conversation file changed, updating data...'));
53
+
54
+ // Extract conversation ID from file path
55
+ const conversationId = this.extractConversationId(filePath);
56
+
57
+ // Enhanced file activity detection for typing
58
+ await this.handleFileActivity(conversationId, filePath);
51
59
 
52
60
  // Invalidate cache for the changed file
53
61
  if (this.dataCache && filePath) {
54
62
  this.dataCache.invalidateFile(filePath);
55
63
  }
56
64
 
65
+ // Notify specific conversation change if callback exists
66
+ if (this.conversationChangeCallback && conversationId) {
67
+ await this.conversationChangeCallback(conversationId, filePath);
68
+ }
69
+
57
70
  await this.triggerDataRefresh();
58
- console.log(chalk.green('✅ Data updated'));
59
71
  });
60
72
 
61
73
  conversationWatcher.on('add', async () => {
62
- console.log(chalk.yellow('📝 New conversation file detected...'));
63
74
  await this.triggerDataRefresh();
64
- console.log(chalk.green('✅ Data updated'));
65
75
  });
66
76
 
67
77
  this.watchers.push(conversationWatcher);
@@ -78,15 +88,11 @@ class FileWatcher {
78
88
  });
79
89
 
80
90
  projectWatcher.on('addDir', async () => {
81
- console.log(chalk.yellow('📁 New project directory detected...'));
82
91
  await this.triggerDataRefresh();
83
- console.log(chalk.green('✅ Data updated'));
84
92
  });
85
93
 
86
94
  projectWatcher.on('change', async () => {
87
- console.log(chalk.yellow('📁 Project directory changed...'));
88
95
  await this.triggerDataRefresh();
89
- console.log(chalk.green('✅ Data updated'));
90
96
  });
91
97
 
92
98
  this.watchers.push(projectWatcher);
@@ -98,7 +104,6 @@ class FileWatcher {
98
104
  setupPeriodicRefresh() {
99
105
  // Periodic refresh to catch any missed changes (reduced frequency)
100
106
  const dataRefreshInterval = setInterval(async () => {
101
- console.log(chalk.blue('⏱️ Periodic data refresh...'));
102
107
  await this.triggerDataRefresh();
103
108
  }, 120000); // Every 2 minutes (reduced from 30 seconds)
104
109
 
@@ -114,6 +119,137 @@ class FileWatcher {
114
119
  this.intervals.push(processRefreshInterval);
115
120
  }
116
121
 
122
+ /**
123
+ * Extract conversation ID from file path
124
+ * @param {string} filePath - Path to the conversation file
125
+ * @returns {string|null} Conversation ID or null if not found
126
+ */
127
+ extractConversationId(filePath) {
128
+ try {
129
+ // Handle different path formats:
130
+ // /Users/user/.claude/projects/PROJECT_NAME/conversation.jsonl -> PROJECT_NAME
131
+ // /Users/user/.claude/CONVERSATION_ID.jsonl -> CONVERSATION_ID
132
+
133
+ const pathParts = filePath.split(path.sep);
134
+ const fileName = pathParts[pathParts.length - 1];
135
+
136
+ if (fileName === 'conversation.jsonl') {
137
+ // Project-based conversation
138
+ const projectName = pathParts[pathParts.length - 2];
139
+ return projectName;
140
+ } else if (fileName.endsWith('.jsonl')) {
141
+ // Direct conversation file
142
+ return fileName.replace('.jsonl', '');
143
+ }
144
+
145
+ return null;
146
+ } catch (error) {
147
+ console.error(chalk.red('Error extracting conversation ID:'), error);
148
+ return null;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Handle file activity for typing detection
154
+ * @param {string} conversationId - Conversation ID
155
+ * @param {string} filePath - File path that changed
156
+ */
157
+ async handleFileActivity(conversationId, filePath) {
158
+ if (!conversationId) return;
159
+
160
+ const fs = require('fs');
161
+ try {
162
+ // Get file stats
163
+ const stats = fs.statSync(filePath);
164
+ const now = Date.now();
165
+ const fileSize = stats.size;
166
+ const mtime = stats.mtime.getTime();
167
+
168
+ // Get previous activity
169
+ const previousActivity = this.fileActivity.get(conversationId) || {
170
+ lastSize: 0,
171
+ lastMtime: 0,
172
+ lastMessageCheck: 0
173
+ };
174
+
175
+ // Check if this is just a file touch/modification without significant content change
176
+ const sizeChanged = fileSize !== previousActivity.lastSize;
177
+ const timeChanged = mtime !== previousActivity.lastMtime;
178
+ const timeSinceLastCheck = now - previousActivity.lastMessageCheck;
179
+
180
+ // Update activity tracking
181
+ this.fileActivity.set(conversationId, {
182
+ lastSize: fileSize,
183
+ lastMtime: mtime,
184
+ lastMessageCheck: now
185
+ });
186
+
187
+ // If file changed but we haven't checked for complete messages recently
188
+ if ((sizeChanged || timeChanged) && timeSinceLastCheck > 1000) {
189
+ // Clear any existing typing timeout
190
+ const existingTimeout = this.typingTimeout.get(conversationId);
191
+ if (existingTimeout) {
192
+ clearTimeout(existingTimeout);
193
+ }
194
+
195
+ // Set a timeout to detect if this is typing activity
196
+ const typingTimeout = setTimeout(async () => {
197
+ // After delay, check if a complete message was added
198
+ await this.checkForTypingActivity(conversationId, filePath);
199
+ }, 2000); // Wait 2 seconds to see if a complete message appears
200
+
201
+ this.typingTimeout.set(conversationId, typingTimeout);
202
+ }
203
+ } catch (error) {
204
+ console.error(chalk.red(`Error handling file activity for ${conversationId}:`), error);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if file activity indicates user typing
210
+ * @param {string} conversationId - Conversation ID
211
+ * @param {string} filePath - File path to check
212
+ */
213
+ async checkForTypingActivity(conversationId, filePath) {
214
+ try {
215
+ // Parse the conversation to see if new complete messages were added
216
+ const ConversationAnalyzer = require('./ConversationAnalyzer');
217
+ const analyzer = new ConversationAnalyzer();
218
+ const messages = await analyzer.getParsedConversation(filePath);
219
+
220
+ if (messages && messages.length > 0) {
221
+ const lastMessage = messages[messages.length - 1];
222
+ const lastMessageTime = new Date(lastMessage.timestamp).getTime();
223
+ const now = Date.now();
224
+ const messageAge = now - lastMessageTime;
225
+
226
+ // If the last message is very recent (< 5 seconds), it's probably a new complete message
227
+ // If it's older, the file activity might indicate typing
228
+ if (messageAge > 5000 && lastMessage.role === 'assistant') {
229
+ // File activity after assistant message suggests user is typing
230
+
231
+ // Send typing notification if we have access to notification manager
232
+ if (this.notificationManager) {
233
+ this.notificationManager.notifyConversationStateChange(conversationId, 'User typing...', {
234
+ detectionMethod: 'file_activity',
235
+ timestamp: new Date().toISOString()
236
+ });
237
+ }
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.error(chalk.red(`Error checking typing activity for ${conversationId}:`), error);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Set notification manager for state notifications
247
+ * @param {Object} notificationManager - NotificationManager instance
248
+ */
249
+ setNotificationManager(notificationManager) {
250
+ this.notificationManager = notificationManager;
251
+ }
252
+
117
253
  /**
118
254
  * Trigger data refresh with error handling
119
255
  */
@@ -274,7 +410,6 @@ class FileWatcher {
274
410
  * Force immediate refresh
275
411
  */
276
412
  async forceRefresh() {
277
- console.log(chalk.cyan('🔄 Force refreshing data...'));
278
413
  await this.triggerDataRefresh();
279
414
  if (this.processRefreshCallback) {
280
415
  await this.processRefreshCallback();
@@ -119,28 +119,12 @@ class DataCache {
119
119
  return cached.messages;
120
120
  }
121
121
 
122
- // Cache miss - parse conversation
122
+ // Cache miss - parse conversation with tool correlation
123
123
  this.metrics.misses++;
124
124
  const content = await this.getFileContent(filepath);
125
+ const lines = content.trim().split('\n').filter(line => line.trim());
125
126
 
126
- const messages = content.trim().split('\n')
127
- .filter(line => line.trim())
128
- .map(line => {
129
- try {
130
- const item = JSON.parse(line);
131
- if (item.message && item.message.role) {
132
- return {
133
- role: item.message.role,
134
- timestamp: new Date(item.timestamp),
135
- content: item.message.content,
136
- model: item.message.model || null,
137
- usage: item.message.usage || null,
138
- };
139
- }
140
- } catch {}
141
- return null;
142
- })
143
- .filter(Boolean);
127
+ const messages = this.parseAndCorrelateToolMessages(lines);
144
128
 
145
129
  this.caches.parsedConversations.set(filepath, {
146
130
  messages,
@@ -150,6 +134,127 @@ class DataCache {
150
134
  return messages;
151
135
  }
152
136
 
137
+ /**
138
+ * Parse JSONL lines and correlate tool_use with tool_result
139
+ * @param {Array} lines - JSONL lines
140
+ * @returns {Array} Parsed and correlated messages
141
+ */
142
+ parseAndCorrelateToolMessages(lines) {
143
+ const entries = [];
144
+ const toolUseMap = new Map();
145
+
146
+ // First pass: parse all entries and map tool_use entries
147
+ for (const line of lines) {
148
+ try {
149
+ const item = JSON.parse(line);
150
+ if (item.message && (item.type === 'assistant' || item.type === 'user')) {
151
+ entries.push(item);
152
+
153
+ // Track tool_use entries by their ID
154
+ if (item.type === 'assistant' && item.message.content) {
155
+ const toolUseBlock = Array.isArray(item.message.content)
156
+ ? item.message.content.find(c => c.type === 'tool_use')
157
+ : (item.message.content.type === 'tool_use' ? item.message.content : null);
158
+
159
+ if (toolUseBlock && toolUseBlock.id) {
160
+ toolUseMap.set(toolUseBlock.id, item);
161
+ if (toolUseBlock.id === 'toolu_01D8RMQYDySWAscQCC6pfDWf') {
162
+ // Debug: Specific tool_use mapped for debugging
163
+ }
164
+ }
165
+ }
166
+ }
167
+ } catch (error) {
168
+ // Skip invalid JSONL lines
169
+ }
170
+ }
171
+
172
+ // Second pass: correlate tool_result with tool_use (first process ALL tool_results)
173
+ for (const item of entries) {
174
+ if (item.type === 'user' && item.message.content) {
175
+ // Check if this is a tool_result entry
176
+ const toolResultBlock = Array.isArray(item.message.content)
177
+ ? item.message.content.find(c => c.type === 'tool_result')
178
+ : (item.message.content.type === 'tool_result' ? item.message.content : null);
179
+
180
+ if (toolResultBlock && toolResultBlock.tool_use_id) {
181
+ // This is a tool_result - attach it to the corresponding tool_use
182
+ const toolUseEntry = toolUseMap.get(toolResultBlock.tool_use_id);
183
+ if (toolUseEntry) {
184
+ // Enhance tool result with additional metadata
185
+ const enhancedToolResult = {
186
+ ...toolResultBlock,
187
+ // Include additional metadata from toolUseResult if available
188
+ ...(item.toolUseResult && {
189
+ stdout: item.toolUseResult.stdout,
190
+ stderr: item.toolUseResult.stderr,
191
+ interrupted: item.toolUseResult.interrupted,
192
+ isImage: item.toolUseResult.isImage,
193
+ returnCodeInterpretation: item.toolUseResult.returnCodeInterpretation
194
+ })
195
+ };
196
+
197
+ // Attach tool result to the tool use entry
198
+ if (!toolUseEntry.toolResults) {
199
+ toolUseEntry.toolResults = [];
200
+ }
201
+ toolUseEntry.toolResults.push(enhancedToolResult);
202
+ // console.log: Tool result attached successfully
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ // Third pass: process messages and filter out standalone tool_result entries
209
+ const processedMessages = [];
210
+
211
+ for (const item of entries) {
212
+ if (item.type === 'user' && item.message.content) {
213
+ // Check if this is a tool_result entry (skip it as we've already processed it)
214
+ const toolResultBlock = Array.isArray(item.message.content)
215
+ ? item.message.content.find(c => c.type === 'tool_result')
216
+ : (item.message.content.type === 'tool_result' ? item.message.content : null);
217
+
218
+ if (toolResultBlock && toolResultBlock.tool_use_id) {
219
+ // Skip standalone tool_result entries - they've been attached to their tool_use
220
+ continue;
221
+ }
222
+ }
223
+
224
+ // Convert to our standard format
225
+ if (item.toolResults) {
226
+ // console.log: Processing item with tool results
227
+ }
228
+
229
+ // Debug specific item we're looking for
230
+ if (item.message && item.message.content && Array.isArray(item.message.content)) {
231
+ const toolUseBlock = item.message.content.find(c => c.type === 'tool_use' && c.id === 'toolu_01D8RMQYDySWAscQCC6pfDWf');
232
+ if (toolUseBlock) {
233
+ // Debug: Processing tool_use item
234
+ }
235
+ }
236
+ const parsed = {
237
+ id: item.message.id || item.uuid || null,
238
+ role: item.message.role || (item.type === 'assistant' ? 'assistant' : 'user'),
239
+ timestamp: new Date(item.timestamp),
240
+ content: item.message.content,
241
+ model: item.message.model || null,
242
+ usage: item.message.usage || null,
243
+ toolResults: item.toolResults || null, // Include attached tool results (populated during correlation)
244
+ isCompactSummary: item.isCompactSummary || false, // Preserve compact summary flag
245
+ uuid: item.uuid || null, // Include UUID for message identification
246
+ type: item.type || null // Include type field
247
+ };
248
+
249
+ // Debug log for our specific tool_use
250
+ // Debug: Final message processing completed
251
+
252
+ processedMessages.push(parsed);
253
+ }
254
+
255
+ return processedMessages;
256
+ }
257
+
153
258
  /**
154
259
  * Get file stats with caching
155
260
  * @param {string} filepath - Path to file
@@ -104,6 +104,43 @@ class NotificationManager {
104
104
  console.log(chalk.green(`📊 Data refreshed (source: ${source})`));
105
105
  }
106
106
 
107
+ /**
108
+ * Send new message notification for real-time updates
109
+ * @param {string} conversationId - Conversation ID
110
+ * @param {Object} message - New message object
111
+ * @param {Object} metadata - Additional metadata
112
+ */
113
+ notifyNewMessage(conversationId, message, metadata = {}) {
114
+ const notification = {
115
+ type: 'new_message',
116
+ conversationId,
117
+ message,
118
+ metadata,
119
+ timestamp: new Date().toISOString(),
120
+ id: this.generateNotificationId()
121
+ };
122
+
123
+ // Don't throttle new message notifications - they should be immediate
124
+ this.addToHistory(notification);
125
+
126
+ // Send via WebSocket to conversation_updates channel
127
+ if (this.webSocketServer) {
128
+ this.webSocketServer.broadcast({
129
+ type: 'new_message',
130
+ data: {
131
+ conversationId,
132
+ message,
133
+ metadata
134
+ }
135
+ }, 'conversation_updates');
136
+ }
137
+
138
+ // Send to local subscribers
139
+ this.notifySubscribers('new_message', notification);
140
+
141
+ console.log(chalk.blue(`📨 New message notification sent for conversation ${conversationId}`));
142
+ }
143
+
107
144
  /**
108
145
  * Send system status notification
109
146
  * @param {Object} status - System status
@@ -281,7 +281,7 @@ class WebSocketServer {
281
281
  });
282
282
 
283
283
  if (sentCount > 0) {
284
- console.log(chalk.green(`📢 Broadcasted ${message.type} to ${sentCount} clients${channel ? ` on channel ${channel}` : ''}`));
284
+ //console.log(chalk.green(`📢 Broadcasted ${message.type} to ${sentCount} clients${channel ? ` on channel ${channel}` : ''}`));
285
285
  }
286
286
 
287
287
  // Queue message if no clients connected
@@ -0,0 +1,46 @@
1
+ # Analytics Web Architecture
2
+
3
+ ## Current Architecture (Active)
4
+
5
+ ### Main Components:
6
+ - **App.js** - Main application orchestrator with sidebar navigation
7
+ - **Sidebar.js** - Navigation sidebar component
8
+ - **DashboardPage.js** - Dashboard page with metrics and charts
9
+ - **AgentsPage.js** - Agents/conversations page
10
+
11
+ ### Services:
12
+ - **WebSocketService.js** - Real-time communication
13
+ - **DataService.js** - API data fetching and caching
14
+ - **StateService.js** - Application state management
15
+
16
+ ### Layout Structure:
17
+ ```
18
+ App.js
19
+ ├── Sidebar.js (navigation)
20
+ └── Page Components
21
+ ├── DashboardPage.js
22
+ └── AgentsPage.js
23
+ ```
24
+
25
+ ## Deprecated Architecture (Removed)
26
+
27
+ ### Deprecated Files:
28
+ - **main.js** → `main.js.deprecated` - Old initialization system
29
+ - **Dashboard.js** → `Dashboard.js.deprecated` - Old monolithic dashboard
30
+
31
+ ### Reason for Deprecation:
32
+ The old architecture used a single Dashboard.js component without navigation, while the new architecture uses App.js with proper routing and a sidebar navigation system.
33
+
34
+ ## WebSocket Integration
35
+
36
+ The WebSocket system is fully functional and provides real-time updates for:
37
+ - Conversation state changes
38
+ - Data refresh events
39
+ - System status updates
40
+
41
+ ## Loading State Fix
42
+
43
+ Fixed issue where loading states weren't clearing properly by:
44
+ 1. Reordering DOM rendering before setting loading states
45
+ 2. Adding proper error handling and fallback mechanisms
46
+ 3. Ensuring `setLoading(false)` is called in finally blocks