claude-code-templates 1.10.1 → 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.
- package/README.md +6 -0
- package/bin/create-claude-config.js +1 -0
- package/package.json +2 -2
- package/src/analytics/core/ConversationAnalyzer.js +94 -20
- package/src/analytics/core/FileWatcher.js +146 -11
- package/src/analytics/data/DataCache.js +124 -19
- package/src/analytics/notifications/NotificationManager.js +37 -0
- package/src/analytics/notifications/WebSocketServer.js +1 -1
- package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
- package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
- package/src/analytics-web/components/AgentsPage.js +2535 -0
- package/src/analytics-web/components/App.js +430 -0
- package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
- package/src/analytics-web/components/DashboardPage.js +1527 -0
- package/src/analytics-web/components/Sidebar.js +197 -0
- package/src/analytics-web/components/ToolDisplay.js +539 -0
- package/src/analytics-web/index.html +3275 -1792
- package/src/analytics-web/services/DataService.js +89 -16
- package/src/analytics-web/services/StateService.js +9 -0
- package/src/analytics-web/services/WebSocketService.js +17 -5
- package/src/analytics.js +323 -35
- package/src/console-bridge.js +610 -0
- package/src/file-operations.js +143 -23
- package/src/index.js +24 -1
- package/src/templates.js +4 -0
- package/src/test-console-bridge.js +67 -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|