claude-mpm 4.1.12__py3-none-any.whl → 4.1.14__py3-none-any.whl

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 (31) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/monitor.py +88 -627
  3. claude_mpm/cli/commands/mpm_init.py +7 -2
  4. claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
  5. claude_mpm/dashboard/static/built/components/code-tree.js +1 -1
  6. claude_mpm/dashboard/static/built/dashboard.js +1 -1
  7. claude_mpm/dashboard/static/built/socket-client.js +1 -1
  8. claude_mpm/dashboard/static/css/activity.css +1239 -267
  9. claude_mpm/dashboard/static/css/code-tree.css +235 -2
  10. claude_mpm/dashboard/static/css/dashboard.css +511 -0
  11. claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/code-tree.js +2 -2593
  13. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  14. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  15. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  16. claude_mpm/dashboard/static/js/components/activity-tree.js +1193 -892
  17. claude_mpm/dashboard/static/js/components/code-tree.js +535 -258
  18. claude_mpm/dashboard/static/js/components/module-viewer.js +21 -7
  19. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +1066 -0
  20. claude_mpm/dashboard/static/js/connection-manager.js +1 -1
  21. claude_mpm/dashboard/static/js/dashboard.js +196 -43
  22. claude_mpm/dashboard/static/js/socket-client.js +2 -2
  23. claude_mpm/dashboard/templates/index.html +95 -25
  24. claude_mpm/services/cli/socketio_manager.py +1 -1
  25. claude_mpm/services/infrastructure/monitoring.py +1 -1
  26. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/METADATA +1 -1
  27. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/RECORD +31 -30
  28. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/WHEEL +0 -0
  29. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/entry_points.txt +0 -0
  30. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/licenses/LICENSE +0 -0
  31. {claude_mpm-4.1.12.dist-info → claude_mpm-4.1.14.dist-info}/top_level.txt +0 -0
@@ -1,49 +1,40 @@
1
1
  /**
2
- * Activity Tree Component
2
+ * Activity Tree Component - Linear Tree View
3
3
  *
4
- * D3.js-based collapsible tree visualization for showing PM activity hierarchy.
5
- * Displays PM actions, TodoWrite delegations, agent assignments, and tool usage.
4
+ * HTML/CSS-based linear tree visualization for showing PM activity hierarchy.
5
+ * Replaces D3.js with simpler, cleaner linear tree structure.
6
+ * Uses simple display methods for data visualization.
6
7
  */
7
8
 
8
9
  class ActivityTree {
9
10
  constructor() {
10
11
  this.container = null;
11
- this.svg = null;
12
- this.treeData = null;
13
- this.root = null;
14
- this.treeLayout = null;
15
- this.treeGroup = null;
16
12
  this.events = [];
17
- this.todoWriteStack = [];
18
- this.activeAgent = null;
19
- this.activeAgentStack = [];
20
- this.margin = {top: 20, right: 120, bottom: 20, left: 120};
21
- this.width = 960 - this.margin.left - this.margin.right;
22
- this.height = 500 - this.margin.top - this.margin.bottom;
23
- this.nodeId = 0;
24
- this.duration = 750;
13
+ this.sessions = new Map();
14
+ this.currentSession = null;
15
+ this.selectedSessionFilter = 'all';
25
16
  this.timeRange = '30min';
26
17
  this.searchTerm = '';
27
- this.tooltip = null;
28
18
  this.initialized = false;
19
+ this.expandedSessions = new Set();
20
+ this.expandedAgents = new Set();
21
+ this.expandedTools = new Set();
22
+ this.selectedItem = null;
29
23
  }
30
24
 
31
25
  /**
32
- * Initialize the activity tree visualization
26
+ * Initialize the activity tree
33
27
  */
34
28
  initialize() {
35
29
  console.log('ActivityTree.initialize() called, initialized:', this.initialized);
36
30
 
37
- // Check if already initialized
38
31
  if (this.initialized) {
39
32
  console.log('Activity tree already initialized, skipping');
40
33
  return;
41
34
  }
42
35
 
43
- // First try to find the container
44
36
  this.container = document.getElementById('activity-tree-container');
45
37
  if (!this.container) {
46
- // Fall back to the inner div if container not found
47
38
  this.container = document.getElementById('activity-tree');
48
39
  if (!this.container) {
49
40
  console.error('Activity tree container not found in DOM');
@@ -51,14 +42,6 @@ class ActivityTree {
51
42
  }
52
43
  }
53
44
 
54
- // Clear any existing text content that might be in the container
55
- if (this.container.textContent && this.container.textContent.trim()) {
56
- console.log('Clearing existing text content from container:', this.container.textContent);
57
- this.container.textContent = '';
58
- }
59
-
60
- console.log('Activity tree container found:', this.container);
61
-
62
45
  // Check if the container is visible before initializing
63
46
  const tabPanel = document.getElementById('activity-tab');
64
47
  if (!tabPanel) {
@@ -66,48 +49,17 @@ class ActivityTree {
66
49
  return;
67
50
  }
68
51
 
69
- // Initialize even if tab is not active, but don't render until visible
52
+ // Initialize even if tab is not active
70
53
  if (!tabPanel.classList.contains('active')) {
71
54
  console.log('Activity tab not active, initializing but deferring render');
72
- // Clear any text content that might be showing
73
- if (this.container.textContent && this.container.textContent.trim()) {
74
- this.container.textContent = '';
75
- }
76
- // Set up basic structure but defer visualization
77
55
  this.setupControls();
78
- this.initializeTreeData();
79
56
  this.subscribeToEvents();
80
57
  this.initialized = true;
81
58
  return;
82
59
  }
83
60
 
84
- // Clear container before creating visualization
85
- if (this.container.textContent && this.container.textContent.trim()) {
86
- console.log('Clearing container text before creating visualization');
87
- this.container.textContent = '';
88
- }
89
-
90
61
  this.setupControls();
91
- this.createVisualization();
92
-
93
- if (!this.svg || !this.treeGroup) {
94
- console.error('Failed to create D3 visualization elements');
95
- // Show error message in container
96
- if (this.container) {
97
- this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #e53e3e;">⚠️ Failed to create visualization. Please refresh the page.</div>';
98
- }
99
- return;
100
- }
101
-
102
- this.initializeTreeData();
103
-
104
- // Only update if we have a valid root
105
- if (this.root) {
106
- this.update(this.root);
107
- } else {
108
- console.warn('Root not created, skipping initial update');
109
- }
110
-
62
+ this.createLinearTreeView();
111
63
  this.subscribeToEvents();
112
64
 
113
65
  this.initialized = true;
@@ -120,7 +72,6 @@ class ActivityTree {
120
72
  forceShow() {
121
73
  console.log('ActivityTree.forceShow() called');
122
74
 
123
- // Ensure container is available
124
75
  if (!this.container) {
125
76
  this.container = document.getElementById('activity-tree-container') || document.getElementById('activity-tree');
126
77
  if (!this.container) {
@@ -129,217 +80,112 @@ class ActivityTree {
129
80
  }
130
81
  }
131
82
 
132
- // Clear any text content
133
- if (this.container.textContent && this.container.textContent.trim()) {
134
- console.log('Clearing text from container:', this.container.textContent);
135
- this.container.innerHTML = '';
136
- }
137
-
138
- // Create visualization if needed
139
- if (!this.svg) {
140
- this.createVisualization();
141
- }
142
-
143
- // Initialize tree data if needed
144
- if (!this.root) {
145
- this.initializeTreeData();
146
- }
147
-
148
- // Update the tree
149
- if (this.root && this.svg && this.treeGroup) {
150
- this.update(this.root);
151
- }
152
-
153
- // Ensure the SVG is visible
154
- if (this.svg) {
155
- const svgNode = this.svg.node();
156
- if (svgNode) {
157
- svgNode.style.display = 'block';
158
- svgNode.style.visibility = 'visible';
159
- }
160
- }
83
+ this.createLinearTreeView();
84
+ this.renderTree();
161
85
  }
162
86
 
163
87
  /**
164
- * Render the visualization when tab becomes visible (called when switching to Activity tab)
88
+ * Render the visualization when tab becomes visible
165
89
  */
166
90
  renderWhenVisible() {
167
91
  console.log('ActivityTree.renderWhenVisible() called');
168
92
 
169
- // Ensure the container is clean
170
- if (this.container && this.container.textContent && this.container.textContent.trim() && !this.svg) {
171
- console.log('Clearing text content before rendering:', this.container.textContent);
172
- this.container.textContent = '';
173
- }
174
-
175
93
  if (!this.initialized) {
176
94
  console.log('Not initialized yet, calling initialize...');
177
95
  this.initialize();
178
96
  return;
179
97
  }
180
98
 
181
- // If already initialized but no visualization, create it
182
- if (!this.svg) {
183
- console.log('Creating deferred visualization...');
184
- this.createVisualization();
185
- if (this.svg && this.treeGroup && this.root) {
186
- this.update(this.root);
187
- } else if (!this.root) {
188
- console.warn('No root node available, initializing tree data...');
189
- this.initializeTreeData();
190
- if (this.root && this.svg && this.treeGroup) {
191
- this.update(this.root);
192
- }
193
- }
194
- }
195
-
196
- // Force update to ensure tree is rendered with current data
197
- if (this.root && this.svg) {
198
- console.log('Updating tree with current data...');
199
- this.update(this.root);
200
- } else {
201
- console.warn('Cannot update tree - missing components:', {
202
- hasRoot: !!this.root,
203
- hasSvg: !!this.svg,
204
- hasTreeGroup: !!this.treeGroup
205
- });
206
- }
99
+ this.createLinearTreeView();
100
+ this.renderTree();
207
101
  }
208
102
 
209
103
  /**
210
104
  * Setup control handlers
211
105
  */
212
106
  setupControls() {
213
- // Expand all button
107
+ // Time range filter dropdown
108
+ const timeRangeSelect = document.getElementById('time-range');
109
+ if (timeRangeSelect) {
110
+ timeRangeSelect.addEventListener('change', (e) => {
111
+ this.timeRange = e.target.value;
112
+ console.log(`ActivityTree: Time range changed to: ${this.timeRange}`);
113
+ this.renderTree();
114
+ });
115
+ }
116
+
117
+ // Listen for session filter changes from SessionManager
118
+ document.addEventListener('sessionFilterChanged', (e) => {
119
+ this.selectedSessionFilter = e.detail.sessionId || 'all';
120
+ console.log(`ActivityTree: Session filter changed to: ${this.selectedSessionFilter} (from SessionManager)`);
121
+ this.renderTree();
122
+ });
123
+
124
+ // Also listen for sessionChanged for backward compatibility
125
+ document.addEventListener('sessionChanged', (e) => {
126
+ this.selectedSessionFilter = e.detail.sessionId || 'all';
127
+ console.log(`ActivityTree: Session changed to: ${this.selectedSessionFilter} (from SessionManager - backward compat)`);
128
+ this.renderTree();
129
+ });
130
+
131
+ // Initialize with current session filter from SessionManager
132
+ setTimeout(() => {
133
+ if (window.sessionManager) {
134
+ const currentFilter = window.sessionManager.getCurrentFilter();
135
+ if (currentFilter !== this.selectedSessionFilter) {
136
+ this.selectedSessionFilter = currentFilter || 'all';
137
+ console.log(`ActivityTree: Initialized with current session filter: ${this.selectedSessionFilter}`);
138
+ this.renderTree();
139
+ }
140
+ }
141
+ }, 100); // Small delay to ensure SessionManager is initialized
142
+
143
+ // Expand all button - expand all sessions
214
144
  const expandAllBtn = document.getElementById('expand-all');
215
145
  if (expandAllBtn) {
216
- expandAllBtn.addEventListener('click', () => this.expandAll());
146
+ expandAllBtn.addEventListener('click', () => this.expandAllSessions());
217
147
  }
218
148
 
219
- // Collapse all button
149
+ // Collapse all button - collapse all sessions
220
150
  const collapseAllBtn = document.getElementById('collapse-all');
221
151
  if (collapseAllBtn) {
222
- collapseAllBtn.addEventListener('click', () => this.collapseAll());
152
+ collapseAllBtn.addEventListener('click', () => this.collapseAllSessions());
223
153
  }
224
154
 
225
- // Reset zoom button
155
+ // Reset zoom button functionality
226
156
  const resetZoomBtn = document.getElementById('reset-zoom');
227
157
  if (resetZoomBtn) {
158
+ resetZoomBtn.style.display = 'inline-block';
228
159
  resetZoomBtn.addEventListener('click', () => this.resetZoom());
229
160
  }
230
161
 
231
- // Time range selector
232
- const timeRangeSelect = document.getElementById('time-range');
233
- if (timeRangeSelect) {
234
- timeRangeSelect.addEventListener('change', (e) => {
235
- this.timeRange = e.target.value;
236
- this.filterEventsByTime();
237
- });
238
- }
239
-
240
162
  // Search input
241
163
  const searchInput = document.getElementById('activity-search');
242
164
  if (searchInput) {
243
165
  searchInput.addEventListener('input', (e) => {
244
166
  this.searchTerm = e.target.value.toLowerCase();
245
- this.highlightSearchResults();
167
+ this.renderTree();
246
168
  });
247
169
  }
248
170
  }
249
171
 
250
172
  /**
251
- * Create the D3 visualization
173
+ * Create the linear tree view container
252
174
  */
253
- createVisualization() {
254
- // Check if D3 is available
255
- if (typeof d3 === 'undefined') {
256
- console.error('D3.js is not loaded! Cannot create activity tree visualization.');
257
- // Try to display an error message in the container
258
- if (this.container) {
259
- this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #e53e3e;">⚠️ D3.js is not loaded. Cannot create visualization.</div>';
260
- }
261
- return;
262
- }
263
-
264
- // Calculate dimensions based on container
265
- const containerRect = this.container.getBoundingClientRect();
266
- this.width = containerRect.width - this.margin.left - this.margin.right;
267
- this.height = Math.max(500, containerRect.height - this.margin.top - this.margin.bottom);
268
-
269
- console.log('Creating D3 visualization with dimensions:', { width: this.width, height: this.height });
270
-
271
- // Clear any existing content (including text)
272
- d3.select(this.container).selectAll('*').remove();
273
-
274
- // Create SVG
275
- this.svg = d3.select(this.container)
276
- .append('svg')
277
- .attr('width', '100%')
278
- .attr('height', '100%')
279
- .attr('viewBox', `0 0 ${this.width + this.margin.left + this.margin.right} ${this.height + this.margin.top + this.margin.bottom}`);
280
-
281
- // Create main group for tree positioning
282
- this.treeGroup = this.svg.append('g')
283
- .attr('class', 'tree-group')
284
- .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
285
-
286
- // Add zoom behavior
287
- const zoom = d3.zoom()
288
- .scaleExtent([0.1, 3])
289
- .on('zoom', (event) => {
290
- this.treeGroup.attr('transform',
291
- `translate(${this.margin.left + event.transform.x},${this.margin.top + event.transform.y}) scale(${event.transform.k})`
292
- );
293
- });
294
-
295
- this.svg.call(zoom);
296
-
297
- // Create tree layout
298
- this.treeLayout = d3.tree()
299
- .size([this.height, this.width]);
175
+ createLinearTreeView() {
176
+ console.log('Creating linear tree view');
300
177
 
301
- console.log('ActivityTree: Tree layout created:', this.treeLayout);
302
-
303
- // Create tooltip
304
- this.tooltip = d3.select('body').append('div')
305
- .attr('class', 'activity-tooltip')
306
- .style('opacity', 0);
178
+ // Clear container
179
+ this.container.innerHTML = '';
307
180
 
308
- console.log('ActivityTree: Visualization complete, svg:', this.svg, 'treeGroup:', this.treeGroup);
309
- }
310
-
311
- /**
312
- * Initialize tree data structure
313
- */
314
- initializeTreeData() {
315
- console.log('ActivityTree: Initializing tree data');
316
-
317
- this.treeData = {
318
- name: 'PM',
319
- type: 'pm',
320
- icon: '🎯',
321
- children: [],
322
- _children: null
323
- };
324
-
325
- // Check if D3 is available
326
- if (typeof d3 === 'undefined') {
327
- console.error('ActivityTree: D3 is not available - cannot create hierarchy!');
328
- // Try to display an error message
329
- if (this.container) {
330
- this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #e53e3e;">⚠️ Waiting for D3.js to load...</div>';
331
- }
332
- return;
333
- }
334
-
335
- this.root = d3.hierarchy(this.treeData);
336
- this.root.x0 = this.height / 2;
337
- this.root.y0 = 0;
181
+ // Create main tree container
182
+ const treeContainer = document.createElement('div');
183
+ treeContainer.id = 'linear-tree';
184
+ treeContainer.className = 'linear-tree';
338
185
 
339
- console.log('ActivityTree: Root node created:', this.root);
186
+ this.container.appendChild(treeContainer);
340
187
 
341
- // Update stats immediately after creating root
342
- this.updateStats();
188
+ console.log('Linear tree view created');
343
189
  }
344
190
 
345
191
  /**
@@ -355,45 +201,98 @@ class ActivityTree {
355
201
  console.log('ActivityTree: Setting up event subscription');
356
202
 
357
203
  // Subscribe to event updates from the socket client
358
- // Process ALL events and determine their type internally
359
- window.socketClient.onEventUpdate((events) => {
360
- console.log(`ActivityTree: onEventUpdate called with ${events.length} total events`);
204
+ // FIXED: Now correctly receives both events AND sessions from socket client
205
+ window.socketClient.onEventUpdate((events, sessions) => {
206
+ console.log(`ActivityTree: onEventUpdate called with ${events.length} total events and ${sessions.size} sessions`);
207
+
208
+ // Use the authoritative sessions from socket client instead of building our own
209
+ this.sessions.clear();
210
+
211
+ // Convert authoritative sessions Map to our format
212
+ for (const [sessionId, sessionData] of sessions.entries()) {
213
+ const activitySession = {
214
+ id: sessionId,
215
+ timestamp: new Date(sessionData.lastActivity || sessionData.startTime || new Date()),
216
+ expanded: this.expandedSessions.has(sessionId) || true, // Preserve expansion state
217
+ agents: new Map(),
218
+ todos: [],
219
+ userInstructions: [],
220
+ tools: [],
221
+ status: 'active',
222
+ currentTodoTool: null,
223
+ // Preserve additional session metadata
224
+ working_directory: sessionData.working_directory,
225
+ git_branch: sessionData.git_branch,
226
+ eventCount: sessionData.eventCount
227
+ };
228
+ this.sessions.set(sessionId, activitySession);
229
+ }
361
230
 
362
231
  // Process only the new events since last update
363
232
  const newEventCount = events.length - this.events.length;
364
233
  if (newEventCount > 0) {
365
- // Process only the new events
366
234
  const newEvents = events.slice(this.events.length);
367
-
368
235
  console.log(`ActivityTree: Processing ${newEventCount} new events`, newEvents);
369
236
 
370
- // Process all events, regardless of format
371
237
  newEvents.forEach(event => {
372
238
  this.processEvent(event);
373
239
  });
374
-
375
- // Update our event count
376
- this.events = [...events];
377
240
  }
241
+
242
+ this.events = [...events];
243
+ this.renderTree();
244
+
245
+ // Debug: Log session state after processing
246
+ console.log(`ActivityTree: Sessions after sync with socket client:`, Array.from(this.sessions.entries()));
378
247
  });
379
248
 
380
- // Load existing events if available
381
- const existingEvents = window.socketClient?.events || window.eventViewer?.events || [];
249
+ // Load existing data from socket client
250
+ const socketState = window.socketClient?.getState();
382
251
 
383
- if (existingEvents.length > 0) {
384
- console.log(`ActivityTree: Processing ${existingEvents.length} existing events`, existingEvents);
385
- existingEvents.forEach(event => {
252
+ if (socketState && socketState.events.length > 0) {
253
+ console.log(`ActivityTree: Loading existing data - ${socketState.events.length} events, ${socketState.sessions.size} sessions`);
254
+
255
+ // Initialize from existing socket client data
256
+ this.sessions.clear();
257
+
258
+ // Convert authoritative sessions Map to our format
259
+ for (const [sessionId, sessionData] of socketState.sessions.entries()) {
260
+ const activitySession = {
261
+ id: sessionId,
262
+ timestamp: new Date(sessionData.lastActivity || sessionData.startTime || new Date()),
263
+ expanded: this.expandedSessions.has(sessionId) || true,
264
+ agents: new Map(),
265
+ todos: [],
266
+ userInstructions: [],
267
+ tools: [],
268
+ status: 'active',
269
+ currentTodoTool: null,
270
+ working_directory: sessionData.working_directory,
271
+ git_branch: sessionData.git_branch,
272
+ eventCount: sessionData.eventCount
273
+ };
274
+ this.sessions.set(sessionId, activitySession);
275
+ }
276
+
277
+ // Process existing events to populate activity data
278
+ socketState.events.forEach(event => {
386
279
  this.processEvent(event);
387
280
  });
388
- this.events = [...existingEvents];
281
+ this.events = [...socketState.events];
282
+ this.renderTree();
283
+
284
+ // Debug: Log initial session state
285
+ console.log(`ActivityTree: Initial sessions state:`, Array.from(this.sessions.entries()));
389
286
  } else {
390
287
  console.log('ActivityTree: No existing events found');
391
288
  this.events = [];
289
+ this.sessions.clear();
290
+ this.renderTree();
392
291
  }
393
292
  }
394
293
 
395
294
  /**
396
- * Process an event and update the tree
295
+ * Process an event and update the session structure
397
296
  */
398
297
  processEvent(event) {
399
298
  if (!event) {
@@ -401,75 +300,68 @@ class ActivityTree {
401
300
  return;
402
301
  }
403
302
 
404
- // Handle events with the actual format from the server
405
- let eventType = null;
406
-
407
- // First check if hook_event_name exists (from transformation)
408
- if (event.hook_event_name) {
409
- eventType = event.hook_event_name;
303
+ // Determine event type
304
+ let eventType = this.getEventType(event);
305
+ if (!eventType) {
306
+ return;
410
307
  }
411
- // Map from type/subtype for hook events
412
- else if (event.type === 'hook' && event.subtype) {
413
- const mapping = {
414
- 'pre_tool': 'PreToolUse',
415
- 'post_tool': 'PostToolUse',
416
- 'subagent_start': 'SubagentStart',
417
- 'subagent_stop': 'SubagentStop',
418
- 'todo_write': 'TodoWrite'
419
- };
420
- eventType = mapping[event.subtype];
421
- }
422
- // Handle todo events
423
- else if (event.type === 'todo' && event.subtype === 'updated') {
424
- eventType = 'TodoWrite';
425
- }
426
- // Handle subagent events
427
- else if (event.type === 'subagent') {
428
- if (event.subtype === 'started') {
429
- eventType = 'SubagentStart';
430
- } else if (event.subtype === 'stopped') {
431
- eventType = 'SubagentStop';
308
+
309
+ console.log(`ActivityTree: Processing event: ${eventType}`, event);
310
+
311
+ // Fix timestamp processing - ensure we get a valid date
312
+ let timestamp;
313
+ if (event.timestamp) {
314
+ // Handle both ISO strings and already parsed dates
315
+ timestamp = new Date(event.timestamp);
316
+ // Check if date is valid
317
+ if (isNaN(timestamp.getTime())) {
318
+ console.warn('ActivityTree: Invalid timestamp, using current time:', event.timestamp);
319
+ timestamp = new Date();
432
320
  }
433
- }
434
- // Handle start event
435
- else if (event.type === 'start') {
436
- eventType = 'Start';
321
+ } else {
322
+ console.warn('ActivityTree: No timestamp found, using current time');
323
+ timestamp = new Date();
437
324
  }
438
325
 
439
- if (!eventType) {
440
- // Only log if it's a potentially relevant event
441
- if (event.type === 'hook' || event.type === 'todo' || event.type === 'subagent') {
442
- console.log('ActivityTree: Cannot determine event type for:', event);
443
- }
326
+ // Get session ID from event - this should match the authoritative sessions
327
+ const sessionId = event.session_id || event.data?.session_id;
328
+
329
+ // Skip events without session ID - they can't be properly categorized
330
+ if (!sessionId) {
331
+ console.log(`ActivityTree: Skipping event without session_id: ${eventType}`);
444
332
  return;
445
333
  }
446
334
 
447
- console.log(`ActivityTree: Processing event: ${eventType}`, event);
448
-
449
- const timestamp = new Date(event.timestamp);
450
- if (!this.isEventInTimeRange(timestamp)) {
335
+ // Find the session - it should already exist from authoritative sessions
336
+ if (!this.sessions.has(sessionId)) {
337
+ console.warn(`ActivityTree: Session ${sessionId} not found in authoritative sessions - skipping event`);
451
338
  return;
452
339
  }
453
340
 
341
+ const session = this.sessions.get(sessionId);
342
+
454
343
  switch (eventType) {
344
+ case 'Start':
345
+ // New PM session started
346
+ this.currentSession = session;
347
+ break;
348
+ case 'user_prompt':
349
+ this.processUserInstruction(event, session);
350
+ break;
455
351
  case 'TodoWrite':
456
- this.processTodoWrite(event);
352
+ this.processTodoWrite(event, session);
457
353
  break;
458
354
  case 'SubagentStart':
459
- this.processSubagentStart(event);
355
+ this.processSubagentStart(event, session);
460
356
  break;
461
357
  case 'SubagentStop':
462
- this.processSubagentStop(event);
358
+ this.processSubagentStop(event, session);
463
359
  break;
464
360
  case 'PreToolUse':
465
- this.processToolUse(event);
361
+ this.processToolUse(event, session);
466
362
  break;
467
363
  case 'PostToolUse':
468
- this.updateToolStatus(event, 'completed');
469
- break;
470
- case 'Start':
471
- this.initializeTreeData();
472
- this.update(this.root);
364
+ this.updateToolStatus(event, session, 'completed');
473
365
  break;
474
366
  }
475
367
 
@@ -477,256 +369,576 @@ class ActivityTree {
477
369
  }
478
370
 
479
371
  /**
480
- * Process TodoWrite event
372
+ * Get event type from event data
481
373
  */
482
- processTodoWrite(event) {
483
- console.log('ActivityTree: Processing TodoWrite event:', event);
374
+ getEventType(event) {
375
+ if (event.hook_event_name) {
376
+ return event.hook_event_name;
377
+ }
484
378
 
485
- // Look for todos in multiple places for compatibility
486
- let todos = event.todos ||
487
- event.data?.todos ||
488
- event.data || // Sometimes todos are directly in data
489
- [];
379
+ if (event.type === 'hook' && event.subtype) {
380
+ const mapping = {
381
+ 'pre_tool': 'PreToolUse',
382
+ 'post_tool': 'PostToolUse',
383
+ 'subagent_start': 'SubagentStart',
384
+ 'subagent_stop': 'SubagentStop',
385
+ 'todo_write': 'TodoWrite'
386
+ };
387
+ return mapping[event.subtype];
388
+ }
490
389
 
491
- // Handle case where todos might be an object with todos property
492
- if (todos && typeof todos === 'object' && todos.todos) {
493
- todos = todos.todos;
390
+ if (event.type === 'todo' && event.subtype === 'updated') {
391
+ return 'TodoWrite';
494
392
  }
495
393
 
496
- // Ensure todos is an array
497
- if (!Array.isArray(todos)) {
498
- console.log('ActivityTree: Invalid todos format in event:', event);
499
- return;
394
+ if (event.type === 'subagent') {
395
+ if (event.subtype === 'started') return 'SubagentStart';
396
+ if (event.subtype === 'stopped') return 'SubagentStop';
500
397
  }
501
398
 
502
- if (todos.length === 0) {
503
- console.log('ActivityTree: No todos in event');
504
- return;
399
+ if (event.type === 'start') {
400
+ return 'Start';
505
401
  }
506
-
507
- // Find in-progress todo
508
- const activeTodo = todos.find(t => t.status === 'in_progress');
509
- if (!activeTodo) {
510
- console.log('ActivityTree: No in-progress todo found');
511
- return;
402
+
403
+ if (event.type === 'user_prompt' || event.subtype === 'user_prompt') {
404
+ return 'user_prompt';
512
405
  }
406
+
407
+ return null;
408
+ }
513
409
 
514
- console.log('ActivityTree: Found active todo:', activeTodo);
410
+ // getSessionId method removed - now using authoritative session IDs directly from socket client
515
411
 
516
- // Create TodoWrite node
517
- const todoNode = {
518
- name: activeTodo.activeForm || activeTodo.content,
519
- type: 'todowrite',
520
- icon: '📝',
521
- content: activeTodo.content,
522
- status: activeTodo.status,
523
- timestamp: event.timestamp,
524
- children: [],
525
- _children: null,
526
- eventId: event.id
412
+ /**
413
+ * Process user instruction/prompt event
414
+ */
415
+ processUserInstruction(event, session) {
416
+ const promptText = event.prompt_text || event.data?.prompt_text || event.prompt || '';
417
+ if (!promptText) return;
418
+
419
+ const instruction = {
420
+ id: `instruction-${session.id}-${Date.now()}`,
421
+ text: promptText,
422
+ preview: promptText.length > 100 ? promptText.substring(0, 100) + '...' : promptText,
423
+ timestamp: event.timestamp || new Date().toISOString(),
424
+ type: 'user_instruction'
527
425
  };
528
-
529
- // Add to PM root
530
- if (!this.root) {
531
- console.error('ActivityTree: No root node!');
532
- return;
533
- }
534
426
 
535
- if (!this.root.data) {
536
- console.error('ActivityTree: Root has no data!');
537
- return;
538
- }
427
+ // Add to session's user instructions
428
+ session.userInstructions.push(instruction);
539
429
 
540
- if (!this.root.data.children) {
541
- this.root.data.children = [];
430
+ // Keep only last 5 instructions to prevent memory bloat
431
+ if (session.userInstructions.length > 5) {
432
+ session.userInstructions = session.userInstructions.slice(-5);
542
433
  }
543
-
544
- console.log('ActivityTree: Adding TodoWrite node to root');
545
- this.root.data.children.push(todoNode);
546
-
547
- // Track this TodoWrite
548
- this.todoWriteStack.push({
549
- node: todoNode,
550
- content: activeTodo.content
551
- });
552
-
553
- console.log('ActivityTree: Calling update with root:', this.root);
554
- this.update(this.root);
555
- console.log('ActivityTree: Update complete');
556
434
  }
557
435
 
558
436
  /**
559
- * Process SubagentStart event
437
+ * Process TodoWrite event - attach TODOs to session and active agent
560
438
  */
561
- processSubagentStart(event) {
562
- // Look for agent_name in multiple places for compatibility
563
- const agentName = event.agent_name ||
564
- event.data?.agent_name ||
565
- event.data?.agent_type ||
566
- event.agent_type || // Check direct agent_type field
567
- event.agent || // Check agent field
568
- 'unknown';
569
- const agentIcon = this.getAgentIcon(agentName);
570
-
571
- // Create agent node
572
- const agentNode = {
573
- name: agentName,
574
- type: 'agent',
575
- icon: agentIcon,
439
+ processTodoWrite(event, session) {
440
+ let todos = event.todos || event.data?.todos || event.data || [];
441
+
442
+ if (todos && typeof todos === 'object' && todos.todos) {
443
+ todos = todos.todos;
444
+ }
445
+
446
+ if (!Array.isArray(todos) || todos.length === 0) {
447
+ return;
448
+ }
449
+
450
+ // Update session's todos directly for overall checklist view
451
+ session.todos = todos.map(todo => ({
452
+ content: todo.content,
453
+ activeForm: todo.activeForm,
454
+ status: todo.status,
455
+ timestamp: event.timestamp
456
+ }));
457
+
458
+ // Create TodoWrite tool for session-level display
459
+ const sessionTodoTool = {
460
+ id: `todo-session-${session.id}-${Date.now()}`,
461
+ name: 'TodoWrite',
462
+ type: 'tool',
463
+ icon: '📝',
576
464
  timestamp: event.timestamp,
577
- children: [],
578
- _children: null,
579
- eventId: event.id,
580
- sessionId: event.session_id || event.data?.session_id
465
+ status: 'active',
466
+ params: {
467
+ todos: todos
468
+ },
469
+ isPrioritizedTool: true
581
470
  };
471
+
472
+ // Update session-level TodoWrite tool
473
+ session.tools = session.tools.filter(t => t.name !== 'TodoWrite');
474
+ session.tools.unshift(sessionTodoTool);
475
+ session.currentTodoTool = sessionTodoTool;
582
476
 
583
- // Find parent - either last TodoWrite or PM root
584
- let parent = null;
585
- if (this.todoWriteStack.length > 0) {
586
- // Check if TodoWrite mentions this agent
587
- const todoWrite = this.todoWriteStack[this.todoWriteStack.length - 1];
588
- if (todoWrite.content && todoWrite.content.toLowerCase().includes(agentName.toLowerCase())) {
589
- parent = todoWrite.node;
477
+ // ALSO attach TodoWrite to the active agent that triggered it
478
+ const agentSessionId = event.session_id || event.data?.session_id;
479
+ let targetAgent = null;
480
+
481
+ // Find the appropriate agent to attach this TodoWrite to
482
+ // First try to find by session ID
483
+ if (agentSessionId && session.agents.has(agentSessionId)) {
484
+ targetAgent = session.agents.get(agentSessionId);
485
+ } else {
486
+ // Fall back to most recent active agent
487
+ const activeAgents = Array.from(session.agents.values())
488
+ .filter(agent => agent.status === 'active' || agent.status === 'in_progress')
489
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
490
+
491
+ if (activeAgents.length > 0) {
492
+ targetAgent = activeAgents[0];
493
+ } else {
494
+ // If no active agents, use the most recently used agent
495
+ const allAgents = Array.from(session.agents.values())
496
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
497
+ if (allAgents.length > 0) {
498
+ targetAgent = allAgents[0];
499
+ }
590
500
  }
591
501
  }
592
502
 
593
- if (!parent) {
594
- parent = this.root.data;
503
+ // Create agent-specific TodoWrite tool
504
+ if (targetAgent) {
505
+ const agentTodoTool = {
506
+ id: `todo-agent-${targetAgent.id}-${Date.now()}`,
507
+ name: 'TodoWrite',
508
+ type: 'tool',
509
+ icon: '📝',
510
+ timestamp: event.timestamp,
511
+ status: 'active',
512
+ params: {
513
+ todos: todos
514
+ },
515
+ isPrioritizedTool: true
516
+ };
517
+
518
+ // Remove existing TodoWrite tool from agent and add the updated one
519
+ targetAgent.tools = targetAgent.tools.filter(t => t.name !== 'TodoWrite');
520
+ targetAgent.tools.unshift(agentTodoTool);
595
521
  }
522
+ }
596
523
 
597
- if (!parent.children) {
598
- parent.children = [];
524
+ /**
525
+ * Process SubagentStart event
526
+ */
527
+ processSubagentStart(event, session) {
528
+ const agentName = event.agent_name || event.data?.agent_name || event.data?.agent_type || event.agent_type || event.agent || 'unknown';
529
+ const agentSessionId = event.session_id || event.data?.session_id;
530
+
531
+ // Use session ID as unique agent identifier, or create unique ID
532
+ const agentId = agentSessionId || `agent-${Date.now()}-${Math.random()}`;
533
+
534
+ // Check if agent already exists in this session
535
+ if (!session.agents.has(agentId)) {
536
+ const agent = {
537
+ id: agentId,
538
+ name: agentName,
539
+ type: 'agent',
540
+ icon: this.getAgentIcon(agentName),
541
+ timestamp: event.timestamp,
542
+ status: 'active',
543
+ tools: [],
544
+ sessionId: agentSessionId,
545
+ isPM: false
546
+ };
547
+
548
+ session.agents.set(agentId, agent);
549
+ } else {
550
+ // Update existing agent status to active
551
+ const existingAgent = session.agents.get(agentId);
552
+ existingAgent.status = 'active';
553
+ existingAgent.timestamp = event.timestamp; // Update timestamp
599
554
  }
600
- parent.children.push(agentNode);
601
-
602
- // Track active agent
603
- this.activeAgent = agentNode;
604
- this.activeAgentStack.push(agentNode);
605
-
606
- this.update(this.root);
607
555
  }
608
556
 
609
557
  /**
610
558
  * Process SubagentStop event
611
559
  */
612
- processSubagentStop(event) {
613
- // Mark agent as completed (look for session_id in multiple places)
614
- const sessionId = event.session_id || event.data?.session_id;
615
- if (this.activeAgent && this.activeAgent.sessionId === sessionId) {
616
- this.activeAgent.status = 'completed';
617
- this.activeAgentStack.pop();
618
- this.activeAgent = this.activeAgentStack.length > 0 ?
619
- this.activeAgentStack[this.activeAgentStack.length - 1] : null;
560
+ processSubagentStop(event, session) {
561
+ const agentSessionId = event.session_id || event.data?.session_id;
562
+
563
+ // Find and mark agent as completed
564
+ if (agentSessionId && session.agents.has(agentSessionId)) {
565
+ const agent = session.agents.get(agentSessionId);
566
+ agent.status = 'completed';
620
567
  }
621
-
622
- this.update(this.root);
623
568
  }
624
569
 
625
570
  /**
626
571
  * Process tool use event
627
572
  */
628
- processToolUse(event) {
629
- // Get tool name from various possible locations
630
- const toolName = event.tool_name ||
631
- event.data?.tool_name ||
632
- event.tool || // Check event.tool field
633
- event.data?.tool ||
634
- 'unknown';
635
-
636
- const toolIcon = this.getToolIcon(toolName);
637
-
638
- // Get parameters from various possible locations
639
- const params = event.tool_parameters ||
640
- event.data?.tool_parameters ||
641
- event.parameters || // Check event.parameters field
642
- event.data?.parameters ||
643
- {};
644
-
645
- // Create tool node
646
- const toolNode = {
573
+ processToolUse(event, session) {
574
+ const toolName = event.tool_name || event.data?.tool_name || event.tool || event.data?.tool || 'unknown';
575
+ const params = event.tool_parameters || event.data?.tool_parameters || event.parameters || event.data?.parameters || {};
576
+ const agentSessionId = event.session_id || event.data?.session_id;
577
+
578
+ const tool = {
579
+ id: `tool-${Date.now()}-${Math.random()}`,
647
580
  name: toolName,
648
581
  type: 'tool',
649
- icon: toolIcon,
582
+ icon: this.getToolIcon(toolName),
650
583
  timestamp: event.timestamp,
651
584
  status: 'in_progress',
652
- children: [],
653
- _children: null,
585
+ params: params,
654
586
  eventId: event.id
655
587
  };
656
588
 
657
- // Add file/command as child if applicable
658
- if (toolName === 'Read' && params.file_path) {
659
- toolNode.children.push({
660
- name: params.file_path,
661
- type: 'file',
662
- icon: '📄',
663
- timestamp: event.timestamp
664
- });
665
- } else if (toolName === 'Edit' && params.file_path) {
666
- toolNode.children.push({
667
- name: params.file_path,
668
- type: 'file',
669
- icon: '✏️',
670
- timestamp: event.timestamp
671
- });
672
- } else if (toolName === 'Write' && params.file_path) {
673
- toolNode.children.push({
674
- name: params.file_path,
675
- type: 'file',
676
- icon: '💾',
677
- timestamp: event.timestamp
678
- });
679
- } else if (toolName === 'Bash' && params.command) {
680
- toolNode.children.push({
681
- name: params.command.substring(0, 50) + (params.command.length > 50 ? '...' : ''),
682
- type: 'command',
683
- icon: '⚡',
684
- timestamp: event.timestamp
685
- });
686
- } else if (toolName === 'WebFetch' && params.url) {
687
- toolNode.children.push({
688
- name: params.url,
689
- type: 'url',
690
- icon: '🌐',
691
- timestamp: event.timestamp
692
- });
589
+ // Find the appropriate agent to attach this tool to
590
+ let targetAgent = null;
591
+
592
+ // First try to find by session ID
593
+ if (agentSessionId && session.agents.has(agentSessionId)) {
594
+ targetAgent = session.agents.get(agentSessionId);
595
+ } else {
596
+ // Fall back to most recent active agent
597
+ const activeAgents = Array.from(session.agents.values())
598
+ .filter(agent => agent.status === 'active')
599
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
600
+
601
+ if (activeAgents.length > 0) {
602
+ targetAgent = activeAgents[0];
603
+ } else {
604
+ // If no active agents, attach to session (PM level)
605
+ session.tools.push(tool);
606
+ return;
607
+ }
693
608
  }
694
609
 
695
- // Find parent - active agent or PM root
696
- let parent = this.activeAgent || this.root.data;
697
- if (!parent.children) {
698
- parent.children = [];
610
+ if (targetAgent) {
611
+ targetAgent.tools.push(tool);
699
612
  }
700
- parent.children.push(toolNode);
701
-
702
- this.update(this.root);
703
613
  }
704
614
 
705
615
  /**
706
616
  * Update tool status after completion
707
617
  */
708
- updateToolStatus(event, status) {
709
- // Find tool node by event ID and update status
710
- const findAndUpdate = (node) => {
711
- if (node.eventId === event.id) {
712
- node.status = status;
713
- return true;
714
- }
715
- if (node.children) {
716
- for (let child of node.children) {
717
- if (findAndUpdate(child)) return true;
718
- }
719
- }
720
- if (node._children) {
721
- for (let child of node._children) {
722
- if (findAndUpdate(child)) return true;
618
+ updateToolStatus(event, session, status) {
619
+ // Find and update tool status across all agents
620
+ const findAndUpdateTool = (agent) => {
621
+ if (agent.tools) {
622
+ const tool = agent.tools.find(t => t.eventId === event.id);
623
+ if (tool) {
624
+ tool.status = status;
625
+ return true;
723
626
  }
724
627
  }
725
628
  return false;
726
629
  };
727
630
 
728
- findAndUpdate(this.root.data);
729
- this.update(this.root);
631
+ // Check all agents in session
632
+ for (let agent of session.agents.values()) {
633
+ if (findAndUpdateTool(agent)) return;
634
+ }
635
+
636
+ // Check session-level tools (PM level)
637
+ if (session.tools && findAndUpdateTool(session)) return;
638
+ }
639
+
640
+ /**
641
+ * Render the linear tree view
642
+ */
643
+ renderTree() {
644
+ const treeContainer = document.getElementById('linear-tree');
645
+ if (!treeContainer) return;
646
+
647
+ // Clear tree
648
+ treeContainer.innerHTML = '';
649
+
650
+ // Add sessions directly (no project root)
651
+ const sortedSessions = Array.from(this.sessions.values())
652
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
653
+
654
+ for (let session of sortedSessions) {
655
+ if (this.selectedSessionFilter !== 'all' && this.selectedSessionFilter !== session.id) {
656
+ continue;
657
+ }
658
+
659
+ const sessionElement = this.createSessionElement(session);
660
+ treeContainer.appendChild(sessionElement);
661
+ }
662
+
663
+ // Session filtering is now handled by the main session selector via event listeners
664
+ }
665
+
666
+
667
+ /**
668
+ * Create session element
669
+ */
670
+ createSessionElement(session) {
671
+ const isExpanded = this.expandedSessions.has(session.id) || session.expanded;
672
+
673
+ // Ensure timestamp is valid and format it consistently
674
+ let sessionTime;
675
+ try {
676
+ const sessionDate = session.timestamp instanceof Date ? session.timestamp : new Date(session.timestamp);
677
+ if (isNaN(sessionDate.getTime())) {
678
+ sessionTime = 'Invalid Date';
679
+ console.warn('ActivityTree: Invalid session timestamp:', session.timestamp);
680
+ } else {
681
+ sessionTime = sessionDate.toLocaleString();
682
+ }
683
+ } catch (error) {
684
+ sessionTime = 'Invalid Date';
685
+ console.error('ActivityTree: Error formatting session timestamp:', error, session.timestamp);
686
+ }
687
+
688
+ const element = document.createElement('div');
689
+ element.className = 'tree-node session';
690
+ element.dataset.sessionId = session.id;
691
+
692
+ const expandIcon = isExpanded ? '▼' : '▶';
693
+ const agentCount = session.agents ? session.agents.size : 0;
694
+ const todoCount = session.todos ? session.todos.length : 0;
695
+ const instructionCount = session.userInstructions ? session.userInstructions.length : 0;
696
+
697
+ console.log(`ActivityTree: Rendering session ${session.id}: ${agentCount} agents, ${instructionCount} instructions, ${todoCount} todos at ${sessionTime}`);
698
+
699
+ element.innerHTML = `
700
+ <div class="tree-node-content" onclick="window.activityTreeInstance.toggleSession('${session.id}')">
701
+ <span class="tree-expand-icon">${expandIcon}</span>
702
+ <span class="tree-icon">🎯</span>
703
+ <span class="tree-label">PM Session</span>
704
+ <span class="tree-meta">${sessionTime} • ${agentCount} agent(s) • ${instructionCount} instruction(s) • ${todoCount} todo(s)</span>
705
+ </div>
706
+ <div class="tree-children" style="display: ${isExpanded ? 'block' : 'none'}">
707
+ ${this.renderSessionContent(session)}
708
+ </div>
709
+ `;
710
+
711
+ return element;
712
+ }
713
+
714
+ /**
715
+ * Render session content (user instructions, todos, agents, tools)
716
+ */
717
+ renderSessionContent(session) {
718
+ let html = '';
719
+
720
+ // Render user instructions first
721
+ if (session.userInstructions && session.userInstructions.length > 0) {
722
+ for (let instruction of session.userInstructions.slice(-3)) { // Show last 3 instructions
723
+ html += this.renderUserInstructionElement(instruction, 1);
724
+ }
725
+ }
726
+
727
+ // Render TODOs as checklist directly under session
728
+ if (session.todos && session.todos.length > 0) {
729
+ html += this.renderTodoChecklistElement(session.todos, 1);
730
+ }
731
+
732
+ // Render session-level tools (PM tools)
733
+ if (session.tools && session.tools.length > 0) {
734
+ for (let tool of session.tools) {
735
+ // Show all tools including TodoWrite - both checklist and tool views are useful
736
+ html += this.renderToolElement(tool, 1);
737
+ }
738
+ }
739
+
740
+ // Render agents
741
+ const agents = Array.from(session.agents.values())
742
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
743
+
744
+ for (let agent of agents) {
745
+ html += this.renderAgentElement(agent, 1);
746
+ }
747
+
748
+ return html;
749
+ }
750
+
751
+ /**
752
+ * Render user instruction element
753
+ */
754
+ renderUserInstructionElement(instruction, level) {
755
+ const isSelected = this.selectedItem && this.selectedItem.type === 'instruction' && this.selectedItem.data.id === instruction.id;
756
+ const selectedClass = isSelected ? 'selected' : '';
757
+
758
+ return `
759
+ <div class="tree-node user-instruction ${selectedClass}" data-level="${level}">
760
+ <div class="tree-node-content">
761
+ <span class="tree-expand-icon"></span>
762
+ <span class="tree-icon">💬</span>
763
+ <span class="tree-label clickable" onclick="window.activityTreeInstance.selectItem(${this.escapeJson(instruction)}, 'instruction', event)">User: "${this.escapeHtml(instruction.preview)}"</span>
764
+ <span class="tree-status status-active">instruction</span>
765
+ </div>
766
+ </div>
767
+ `;
768
+ }
769
+
770
+ /**
771
+ * Render TODO checklist element
772
+ */
773
+ renderTodoChecklistElement(todos, level) {
774
+ const checklistId = `checklist-${Date.now()}`;
775
+ const isExpanded = this.expandedTools.has(checklistId) !== false; // Default to expanded
776
+ const expandIcon = isExpanded ? '▼' : '▶';
777
+
778
+ // Calculate status summary
779
+ let completedCount = 0;
780
+ let inProgressCount = 0;
781
+ let pendingCount = 0;
782
+
783
+ todos.forEach(todo => {
784
+ if (todo.status === 'completed') completedCount++;
785
+ else if (todo.status === 'in_progress') inProgressCount++;
786
+ else pendingCount++;
787
+ });
788
+
789
+ let statusSummary = '';
790
+ if (inProgressCount > 0) {
791
+ statusSummary = `${inProgressCount} in progress, ${completedCount} completed`;
792
+ } else if (completedCount === todos.length && todos.length > 0) {
793
+ statusSummary = `All ${todos.length} completed`;
794
+ } else {
795
+ statusSummary = `${todos.length} todo(s)`;
796
+ }
797
+
798
+ let html = `
799
+ <div class="tree-node todo-checklist" data-level="${level}">
800
+ <div class="tree-node-content">
801
+ <span class="tree-expand-icon" onclick="window.activityTreeInstance.toggleTodoChecklist('${checklistId}'); event.stopPropagation();">${expandIcon}</span>
802
+ <span class="tree-icon">☑️</span>
803
+ <span class="tree-label">TODOs</span>
804
+ <span class="tree-params">${statusSummary}</span>
805
+ <span class="tree-status status-active">checklist</span>
806
+ </div>
807
+ `;
808
+
809
+ // Show expanded todo items if expanded
810
+ if (isExpanded) {
811
+ html += '<div class="tree-children">';
812
+ for (let todo of todos) {
813
+ const statusIcon = this.getCheckboxIcon(todo.status);
814
+ const statusClass = `status-${todo.status}`;
815
+ const displayText = todo.status === 'in_progress' ? todo.activeForm : todo.content;
816
+
817
+ html += `
818
+ <div class="tree-node todo-item ${statusClass}" data-level="${level + 1}">
819
+ <div class="tree-node-content">
820
+ <span class="tree-expand-icon"></span>
821
+ <span class="tree-icon">${statusIcon}</span>
822
+ <span class="tree-label">${this.escapeHtml(displayText)}</span>
823
+ <span class="tree-status ${statusClass}">${todo.status.replace('_', ' ')}</span>
824
+ </div>
825
+ </div>
826
+ `;
827
+ }
828
+ html += '</div>';
829
+ }
830
+
831
+ html += '</div>';
832
+ return html;
833
+ }
834
+
835
+ /**
836
+ * Render agent element
837
+ */
838
+ renderAgentElement(agent, level) {
839
+ const statusClass = agent.status === 'active' ? 'status-active' : 'status-completed';
840
+ const isExpanded = this.expandedAgents.has(agent.id);
841
+ const hasTools = agent.tools && agent.tools.length > 0;
842
+ const isSelected = this.selectedItem && this.selectedItem.type === 'agent' && this.selectedItem.data.id === agent.id;
843
+
844
+ const expandIcon = hasTools ? (isExpanded ? '▼' : '▶') : '';
845
+ const selectedClass = isSelected ? 'selected' : '';
846
+
847
+ let html = `
848
+ <div class="tree-node agent ${statusClass} ${selectedClass}" data-level="${level}">
849
+ <div class="tree-node-content">
850
+ ${expandIcon ? `<span class="tree-expand-icon" onclick="window.activityTreeInstance.toggleAgent('${agent.id}'); event.stopPropagation();">${expandIcon}</span>` : '<span class="tree-expand-icon"></span>'}
851
+ <span class="tree-icon">${agent.icon}</span>
852
+ <span class="tree-label clickable" onclick="window.activityTreeInstance.selectItem(${this.escapeJson(agent)}, 'agent', event)">${agent.name}</span>
853
+ <span class="tree-status ${statusClass}">${agent.status}</span>
854
+ </div>
855
+ `;
856
+
857
+ // Render tools under this agent
858
+ if (hasTools && isExpanded) {
859
+ html += '<div class="tree-children">';
860
+ for (let tool of agent.tools) {
861
+ html += this.renderToolElement(tool, level + 1);
862
+ }
863
+ html += '</div>';
864
+ }
865
+
866
+ html += '</div>';
867
+ return html;
868
+ }
869
+
870
+ /**
871
+ * Render tool element (non-expandable, clickable to show data)
872
+ */
873
+ renderToolElement(tool, level) {
874
+ const statusClass = `status-${tool.status}`;
875
+ const params = this.getToolParams(tool);
876
+ const isSelected = this.selectedItem && this.selectedItem.type === 'tool' && this.selectedItem.data.id === tool.id;
877
+ const selectedClass = isSelected ? 'selected' : '';
878
+
879
+ let html = `
880
+ <div class="tree-node tool ${statusClass} ${selectedClass}" data-level="${level}">
881
+ <div class="tree-node-content">
882
+ <span class="tree-expand-icon"></span>
883
+ <span class="tree-icon">${tool.icon}</span>
884
+ <span class="tree-label clickable" onclick="window.activityTreeInstance.selectItem(${this.escapeJson(tool)}, 'tool', event)">${tool.name} (click to view details)</span>
885
+ <span class="tree-params">${params}</span>
886
+ <span class="tree-status ${statusClass}">${tool.status}</span>
887
+ </div>
888
+ </div>
889
+ `;
890
+
891
+ return html;
892
+ }
893
+
894
+ /**
895
+ * Get formatted tool parameters
896
+ */
897
+ getToolParams(tool) {
898
+ if (!tool.params) return '';
899
+
900
+ if (tool.name === 'Read' && tool.params.file_path) {
901
+ return tool.params.file_path;
902
+ }
903
+ if (tool.name === 'Edit' && tool.params.file_path) {
904
+ return tool.params.file_path;
905
+ }
906
+ if (tool.name === 'Write' && tool.params.file_path) {
907
+ return tool.params.file_path;
908
+ }
909
+ if (tool.name === 'Bash' && tool.params.command) {
910
+ const cmd = tool.params.command;
911
+ return cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd;
912
+ }
913
+ if (tool.name === 'WebFetch' && tool.params.url) {
914
+ return tool.params.url;
915
+ }
916
+
917
+ return '';
918
+ }
919
+
920
+ /**
921
+ * Get status icon for todo status
922
+ */
923
+ getStatusIcon(status) {
924
+ const icons = {
925
+ 'pending': '⏸️',
926
+ 'in_progress': '🔄',
927
+ 'completed': '✅'
928
+ };
929
+ return icons[status] || '❓';
930
+ }
931
+
932
+ /**
933
+ * Get checkbox icon for todo checklist items
934
+ */
935
+ getCheckboxIcon(status) {
936
+ const icons = {
937
+ 'pending': '⏳',
938
+ 'in_progress': '🔄',
939
+ 'completed': '✅'
940
+ };
941
+ return icons[status] || '❓';
730
942
  }
731
943
 
732
944
  /**
@@ -762,508 +974,606 @@ class ActivityTree {
762
974
  }
763
975
 
764
976
  /**
765
- * Update the tree visualization
977
+ * Toggle session expansion
766
978
  */
767
- update(source) {
768
- console.log('ActivityTree: update() called with source:', source);
769
-
770
- // Check if D3 is available
771
- if (typeof d3 === 'undefined') {
772
- console.error('ActivityTree: Cannot update - D3.js not loaded');
773
- return;
774
- }
775
-
776
- // Check if visualization is ready
777
- if (!this.svg || !this.treeGroup) {
778
- console.warn('ActivityTree: Cannot update - SVG not initialized');
779
- // Try to create visualization if container exists
780
- if (this.container) {
781
- console.log('Attempting to create visualization from update()');
782
- this.createVisualization();
783
- // Check again after creation attempt
784
- if (!this.svg || !this.treeGroup) {
785
- console.error('Failed to create visualization in update()');
786
- return;
787
- }
788
- } else {
789
- return;
790
- }
791
- }
792
-
793
- if (!this.treeLayout) {
794
- console.warn('ActivityTree: Cannot update - tree layout not initialized');
795
- // Try to create tree layout
796
- if (typeof d3 !== 'undefined') {
797
- this.treeLayout = d3.tree().size([this.height, this.width]);
798
- console.log('Created tree layout in update()');
799
- } else {
800
- return;
801
- }
802
- }
803
-
804
- // Ensure source has valid data
805
- if (!source || !source.data) {
806
- console.error('ActivityTree: Invalid source in update()', source);
807
- return;
808
- }
809
-
810
- // Ensure we have a valid root
811
- if (!this.root) {
812
- console.error('ActivityTree: No root node available for update');
813
- return;
979
+ toggleSession(sessionId) {
980
+ if (this.expandedSessions.has(sessionId)) {
981
+ this.expandedSessions.delete(sessionId);
982
+ } else {
983
+ this.expandedSessions.add(sessionId);
814
984
  }
815
985
 
816
- // Compute the new tree layout
817
- let treeData;
818
- try {
819
- treeData = this.treeLayout(this.root);
820
- } catch (error) {
821
- console.error('ActivityTree: Error computing tree layout:', error);
822
- return;
986
+ // Update the session in the data structure
987
+ const session = this.sessions.get(sessionId);
988
+ if (session) {
989
+ session.expanded = this.expandedSessions.has(sessionId);
823
990
  }
824
991
 
825
- const nodes = treeData.descendants();
826
- const links = treeData.links();
827
-
828
- console.log(`ActivityTree: Updating tree with ${nodes.length} nodes`);
829
-
830
- // Check if we actually have the tree container
831
- if (nodes.length === 1 && this.container) {
832
- // Only root node exists, ensure container shows the tree
833
- const svgElement = this.container.querySelector('svg');
834
- if (!svgElement) {
835
- console.warn('SVG element not found in container after update');
836
- }
837
- }
838
-
839
- // Normalize for fixed-depth
840
- nodes.forEach((d) => {
841
- d.y = d.depth * 180;
842
- });
843
-
844
- // Update nodes
845
- const node = this.treeGroup.selectAll('g.node')
846
- .data(nodes, (d) => d.id || (d.id = ++this.nodeId));
847
-
848
- // Enter new nodes
849
- const nodeEnter = node.enter().append('g')
850
- .attr('class', 'node')
851
- .attr('transform', (d) => `translate(${source.y0},${source.x0})`)
852
- .on('click', (event, d) => this.click(d));
853
-
854
- // Add circles for nodes
855
- nodeEnter.append('circle')
856
- .attr('class', (d) => `node-circle ${d.data.type}`)
857
- .attr('r', 1e-6)
858
- .style('fill', (d) => d._children ? this.getNodeColor(d.data.type) : '#fff')
859
- .style('stroke', (d) => this.getNodeColor(d.data.type));
860
-
861
- // Add icons
862
- nodeEnter.append('text')
863
- .attr('class', 'node-icon')
864
- .attr('dy', '.35em')
865
- .attr('text-anchor', 'middle')
866
- .style('font-size', '14px')
867
- .text((d) => d.data.icon || '');
868
-
869
- // Add labels
870
- nodeEnter.append('text')
871
- .attr('class', 'node-label')
872
- .attr('dy', '.35em')
873
- .attr('x', (d) => d.children || d._children ? -25 : 25)
874
- .attr('text-anchor', (d) => d.children || d._children ? 'end' : 'start')
875
- .text((d) => d.data.name)
876
- .style('fill-opacity', 1e-6);
877
-
878
- // Add tooltips
879
- nodeEnter.on('mouseover', (event, d) => this.showTooltip(event, d))
880
- .on('mouseout', () => this.hideTooltip());
881
-
882
- // Update existing nodes
883
- const nodeUpdate = nodeEnter.merge(node);
884
-
885
- // Transition nodes to new position
886
- nodeUpdate.transition()
887
- .duration(this.duration)
888
- .attr('transform', (d) => `translate(${d.y},${d.x})`);
889
-
890
- nodeUpdate.select('circle.node-circle')
891
- .attr('r', 10)
892
- .style('fill', (d) => {
893
- if (d.data.status === 'in_progress') {
894
- return this.getNodeColor(d.data.type);
895
- }
896
- return d._children ? this.getNodeColor(d.data.type) : '#fff';
897
- })
898
- .attr('class', (d) => {
899
- let classes = `node-circle ${d.data.type}`;
900
- if (d.data.status === 'in_progress') classes += ' pulsing';
901
- if (d.data.status === 'failed') classes += ' failed';
902
- return classes;
903
- });
904
-
905
- nodeUpdate.select('text.node-label')
906
- .style('fill-opacity', 1);
907
-
908
- // Remove exiting nodes
909
- const nodeExit = node.exit().transition()
910
- .duration(this.duration)
911
- .attr('transform', (d) => `translate(${source.y},${source.x})`)
912
- .remove();
913
-
914
- nodeExit.select('circle')
915
- .attr('r', 1e-6);
992
+ this.renderTree();
993
+ }
916
994
 
917
- nodeExit.select('text')
918
- .style('fill-opacity', 1e-6);
995
+ /**
996
+ * Expand all sessions
997
+ */
998
+ expandAllSessions() {
999
+ for (let sessionId of this.sessions.keys()) {
1000
+ this.expandedSessions.add(sessionId);
1001
+ const session = this.sessions.get(sessionId);
1002
+ if (session) session.expanded = true;
1003
+ }
1004
+ this.renderTree();
1005
+ }
919
1006
 
920
- // Update links
921
- const link = this.treeGroup.selectAll('path.link')
922
- .data(links, (d) => d.target.id);
1007
+ /**
1008
+ * Collapse all sessions
1009
+ */
1010
+ collapseAllSessions() {
1011
+ this.expandedSessions.clear();
1012
+ for (let session of this.sessions.values()) {
1013
+ session.expanded = false;
1014
+ }
1015
+ this.renderTree();
1016
+ }
923
1017
 
924
- // Enter new links
925
- const linkEnter = link.enter().insert('path', 'g')
926
- .attr('class', 'link')
927
- .attr('d', (d) => {
928
- const o = {x: source.x0, y: source.y0};
929
- return this.diagonal({source: o, target: o});
930
- });
931
1018
 
932
- // Update existing links
933
- const linkUpdate = linkEnter.merge(link);
934
-
935
- linkUpdate.transition()
936
- .duration(this.duration)
937
- .attr('d', this.diagonal);
938
-
939
- // Remove exiting links
940
- link.exit().transition()
941
- .duration(this.duration)
942
- .attr('d', (d) => {
943
- const o = {x: source.x, y: source.y};
944
- return this.diagonal({source: o, target: o});
945
- })
946
- .remove();
947
-
948
- // Store old positions for transition
949
- nodes.forEach((d) => {
950
- d.x0 = d.x;
951
- d.y0 = d.y;
952
- });
1019
+ /**
1020
+ * Update statistics
1021
+ */
1022
+ updateStats() {
1023
+ const totalNodes = this.countTotalNodes();
1024
+ const activeNodes = this.countActiveNodes();
1025
+ const maxDepth = this.calculateMaxDepth();
953
1026
 
954
- // Update breadcrumb on node click
955
- this.updateBreadcrumb(source);
1027
+ const nodeCountEl = document.getElementById('node-count');
1028
+ const activeCountEl = document.getElementById('active-count');
1029
+ const depthEl = document.getElementById('tree-depth');
1030
+
1031
+ if (nodeCountEl) nodeCountEl.textContent = totalNodes;
1032
+ if (activeCountEl) activeCountEl.textContent = activeNodes;
1033
+ if (depthEl) depthEl.textContent = maxDepth;
1034
+
1035
+ console.log(`ActivityTree: Stats updated - Nodes: ${totalNodes}, Active: ${activeNodes}, Depth: ${maxDepth}`);
956
1036
  }
957
1037
 
958
1038
  /**
959
- * Create diagonal path for links
1039
+ * Count total nodes across all sessions
960
1040
  */
961
- diagonal(d) {
962
- return `M ${d.source.y} ${d.source.x}
963
- C ${(d.source.y + d.target.y) / 2} ${d.source.x},
964
- ${(d.source.y + d.target.y) / 2} ${d.target.x},
965
- ${d.target.y} ${d.target.x}`;
1041
+ countTotalNodes() {
1042
+ let count = 0; // No project root anymore
1043
+ for (let session of this.sessions.values()) {
1044
+ count += 1; // Session
1045
+ count += session.agents.size; // Agents
1046
+
1047
+ // Count user instructions
1048
+ if (session.userInstructions) {
1049
+ count += session.userInstructions.length;
1050
+ }
1051
+
1052
+ // Count todos
1053
+ if (session.todos) {
1054
+ count += session.todos.length;
1055
+ }
1056
+
1057
+ // Count session-level tools
1058
+ if (session.tools) {
1059
+ count += session.tools.length;
1060
+ }
1061
+
1062
+ // Count tools in agents
1063
+ for (let agent of session.agents.values()) {
1064
+ if (agent.tools) {
1065
+ count += agent.tools.length;
1066
+ }
1067
+ }
1068
+ }
1069
+ return count;
966
1070
  }
967
1071
 
968
1072
  /**
969
- * Handle node click for expand/collapse
1073
+ * Count active nodes (in progress)
970
1074
  */
971
- click(d) {
972
- if (d.children) {
973
- d._children = d.children;
974
- d.children = null;
975
- } else {
976
- d.children = d._children;
977
- d._children = null;
1075
+ countActiveNodes() {
1076
+ let count = 0;
1077
+ for (let session of this.sessions.values()) {
1078
+ // Count active session
1079
+ if (session.status === 'active') count++;
1080
+
1081
+ // Count active todos
1082
+ if (session.todos) {
1083
+ for (let todo of session.todos) {
1084
+ if (todo.status === 'in_progress') count++;
1085
+ }
1086
+ }
1087
+
1088
+ // Count session-level tools
1089
+ if (session.tools) {
1090
+ for (let tool of session.tools) {
1091
+ if (tool.status === 'in_progress') count++;
1092
+ }
1093
+ }
1094
+
1095
+ // Count agents and their tools
1096
+ for (let agent of session.agents.values()) {
1097
+ if (agent.status === 'active') count++;
1098
+ if (agent.tools) {
1099
+ for (let tool of agent.tools) {
1100
+ if (tool.status === 'in_progress') count++;
1101
+ }
1102
+ }
1103
+ }
978
1104
  }
979
- this.update(d);
980
- this.updateBreadcrumb(d);
1105
+ return count;
981
1106
  }
982
1107
 
983
1108
  /**
984
- * Get node color based on type
1109
+ * Calculate maximum depth
985
1110
  */
986
- getNodeColor(type) {
987
- const colors = {
988
- 'pm': '#4299e1',
989
- 'todowrite': '#48bb78',
990
- 'agent': '#ed8936',
991
- 'tool': '#9f7aea',
992
- 'file': '#38b2ac',
993
- 'command': '#f56565',
994
- 'url': '#4299e1'
995
- };
996
- return colors[type] || '#718096';
1111
+ calculateMaxDepth() {
1112
+ let maxDepth = 0; // No project root anymore
1113
+ for (let session of this.sessions.values()) {
1114
+ let sessionDepth = 1; // Session level (now root level)
1115
+
1116
+ // Check session content (instructions, todos, tools)
1117
+ if (session.userInstructions && session.userInstructions.length > 0) {
1118
+ sessionDepth = Math.max(sessionDepth, 2); // Instruction level
1119
+ }
1120
+
1121
+ if (session.todos && session.todos.length > 0) {
1122
+ sessionDepth = Math.max(sessionDepth, 3); // Todo checklist -> todo items
1123
+ }
1124
+
1125
+ if (session.tools && session.tools.length > 0) {
1126
+ sessionDepth = Math.max(sessionDepth, 2); // Tool level
1127
+ }
1128
+
1129
+ // Check agents
1130
+ for (let agent of session.agents.values()) {
1131
+ if (agent.tools && agent.tools.length > 0) {
1132
+ sessionDepth = Math.max(sessionDepth, 3); // Tool level under agents
1133
+ }
1134
+ }
1135
+
1136
+ maxDepth = Math.max(maxDepth, sessionDepth);
1137
+ }
1138
+ return maxDepth;
997
1139
  }
998
1140
 
999
1141
  /**
1000
- * Show tooltip
1142
+ * Toggle agent expansion
1001
1143
  */
1002
- showTooltip(event, d) {
1003
- const content = `
1004
- <strong>${d.data.name}</strong><br>
1005
- Type: ${d.data.type}<br>
1006
- ${d.data.timestamp ? `Time: ${new Date(d.data.timestamp).toLocaleTimeString()}` : ''}
1007
- ${d.data.status ? `<br>Status: ${d.data.status}` : ''}
1008
- `;
1009
-
1010
- this.tooltip.transition()
1011
- .duration(200)
1012
- .style('opacity', .9);
1013
-
1014
- this.tooltip.html(content)
1015
- .style('left', (event.pageX + 10) + 'px')
1016
- .style('top', (event.pageY - 28) + 'px');
1144
+ toggleAgent(agentId) {
1145
+ if (this.expandedAgents.has(agentId)) {
1146
+ this.expandedAgents.delete(agentId);
1147
+ } else {
1148
+ this.expandedAgents.add(agentId);
1149
+ }
1150
+ this.renderTree();
1017
1151
  }
1018
-
1152
+
1019
1153
  /**
1020
- * Hide tooltip
1154
+ * Toggle tool expansion (deprecated - tools are no longer expandable)
1021
1155
  */
1022
- hideTooltip() {
1023
- this.tooltip.transition()
1024
- .duration(500)
1025
- .style('opacity', 0);
1156
+ toggleTool(toolId) {
1157
+ // Tools are no longer expandable - this method is kept for compatibility
1158
+ console.log('Tool expansion is disabled. Tools now show data in the left pane when clicked.');
1026
1159
  }
1027
1160
 
1028
1161
  /**
1029
- * Expand all nodes
1162
+ * Toggle TODO checklist expansion
1030
1163
  */
1031
- expandAll() {
1032
- const expand = (d) => {
1033
- if (d._children) {
1034
- d.children = d._children;
1035
- d._children = null;
1036
- }
1037
- if (d.children) {
1038
- d.children.forEach(expand);
1039
- }
1040
- };
1041
-
1042
- expand(this.root);
1043
- this.update(this.root);
1164
+ toggleTodoChecklist(checklistId) {
1165
+ if (this.expandedTools.has(checklistId)) {
1166
+ this.expandedTools.delete(checklistId);
1167
+ } else {
1168
+ this.expandedTools.add(checklistId);
1169
+ }
1170
+ this.renderTree();
1044
1171
  }
1045
1172
 
1046
1173
  /**
1047
- * Collapse all nodes
1174
+ * Handle item click to show data in left pane
1048
1175
  */
1049
- collapseAll() {
1050
- const collapse = (d) => {
1051
- if (d.children) {
1052
- d._children = d.children;
1053
- d._children.forEach(collapse);
1054
- d.children = null;
1055
- }
1056
- };
1176
+ selectItem(item, itemType, event) {
1177
+ // Stop event propagation to prevent expand/collapse when clicking on label
1178
+ if (event) {
1179
+ event.stopPropagation();
1180
+ }
1057
1181
 
1058
- this.root.children?.forEach(collapse);
1059
- this.update(this.root);
1182
+ this.selectedItem = { data: item, type: itemType };
1183
+ this.displayItemData(item, itemType);
1184
+ this.renderTree(); // Re-render to show selection highlight
1060
1185
  }
1061
1186
 
1062
1187
  /**
1063
- * Reset zoom
1188
+ * Display item data in left pane using simple display methods
1064
1189
  */
1065
- resetZoom() {
1066
- if (!this.svg) {
1067
- console.warn('Cannot reset zoom: SVG not initialized');
1190
+ displayItemData(item, itemType) {
1191
+ // Special handling for TodoWrite tools to match Tools view display
1192
+ if (itemType === 'tool' && item.name === 'TodoWrite' && item.params && item.params.todos) {
1193
+ this.displayTodoWriteData(item);
1068
1194
  return;
1069
1195
  }
1070
1196
 
1071
- const zoom = d3.zoom()
1072
- .scaleExtent([0.1, 3])
1073
- .on('zoom', (event) => {
1074
- this.treeGroup.attr('transform',
1075
- `translate(${this.margin.left + event.transform.x},${this.margin.top + event.transform.y}) scale(${event.transform.k})`
1076
- );
1077
- });
1078
-
1079
- this.svg.transition()
1080
- .duration(750)
1081
- .call(zoom.transform, d3.zoomIdentity);
1197
+ // Use simple display methods based on item type
1198
+ switch(itemType) {
1199
+ case 'agent':
1200
+ this.displayAgentData(item);
1201
+ break;
1202
+ case 'tool':
1203
+ this.displayToolData(item);
1204
+ break;
1205
+ case 'instruction':
1206
+ this.displayInstructionData(item);
1207
+ break;
1208
+ default:
1209
+ this.displayGenericData(item, itemType);
1210
+ break;
1211
+ }
1082
1212
 
1083
- // Reset the tree group transform
1084
- this.treeGroup.transition()
1085
- .duration(750)
1086
- .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
1213
+ // Update module header for consistency
1214
+ const moduleHeader = document.querySelector('.module-data-header h5');
1215
+ if (moduleHeader) {
1216
+ const icons = {
1217
+ 'agent': '🤖',
1218
+ 'tool': '🔧',
1219
+ 'instruction': '💬',
1220
+ 'session': '🎯'
1221
+ };
1222
+ const icon = icons[itemType] || '📊';
1223
+ const name = item.name || item.agentName || item.tool_name || 'Item';
1224
+ moduleHeader.textContent = `${icon} ${itemType}: ${name}`;
1225
+ }
1087
1226
  }
1088
1227
 
1089
1228
  /**
1090
- * Check if event is in time range
1229
+ * Display TodoWrite data in the same clean format as Tools view
1091
1230
  */
1092
- isEventInTimeRange(timestamp) {
1093
- if (this.timeRange === 'all') return true;
1231
+ displayTodoWriteData(item) {
1232
+ const todos = item.params.todos || [];
1233
+ const timestamp = this.formatTimestamp(item.timestamp);
1234
+
1235
+ // Calculate status summary
1236
+ let completedCount = 0;
1237
+ let inProgressCount = 0;
1238
+ let pendingCount = 0;
1239
+
1240
+ todos.forEach(todo => {
1241
+ if (todo.status === 'completed') completedCount++;
1242
+ else if (todo.status === 'in_progress') inProgressCount++;
1243
+ else pendingCount++;
1244
+ });
1245
+
1246
+ let html = `
1247
+ <div class="unified-viewer-header">
1248
+ <h6>📝 TodoWrite: PM ${timestamp}</h6>
1249
+ <span class="unified-viewer-status">${this.formatStatus(item.status)}</span>
1250
+ </div>
1251
+ <div class="unified-viewer-content">
1252
+ `;
1253
+
1254
+ if (todos.length > 0) {
1255
+ // Status summary
1256
+ html += `
1257
+ <div class="detail-section">
1258
+ <span class="detail-section-title">Todo Summary</span>
1259
+ <div class="todo-summary">
1260
+ <div class="summary-item completed">
1261
+ <span class="summary-icon">✅</span>
1262
+ <span class="summary-count">${completedCount}</span>
1263
+ <span class="summary-label">Completed</span>
1264
+ </div>
1265
+ <div class="summary-item in_progress">
1266
+ <span class="summary-icon">🔄</span>
1267
+ <span class="summary-count">${inProgressCount}</span>
1268
+ <span class="summary-label">In Progress</span>
1269
+ </div>
1270
+ <div class="summary-item pending">
1271
+ <span class="summary-icon">⏳</span>
1272
+ <span class="summary-count">${pendingCount}</span>
1273
+ <span class="summary-label">Pending</span>
1274
+ </div>
1275
+ </div>
1276
+ </div>
1277
+ `;
1278
+
1279
+ // Todo list display (same as Tools view)
1280
+ html += `
1281
+ <div class="detail-section">
1282
+ <span class="detail-section-title">Todo List (${todos.length} items)</span>
1283
+ <div class="todo-checklist">
1284
+ `;
1285
+
1286
+ todos.forEach((todo, index) => {
1287
+ const statusIcon = this.getCheckboxIcon(todo.status);
1288
+ const displayText = todo.status === 'in_progress' ?
1289
+ (todo.activeForm || todo.content) : todo.content;
1290
+ const statusClass = this.formatStatusClass(todo.status);
1291
+
1292
+ html += `
1293
+ <div class="todo-checklist-item ${todo.status}">
1294
+ <div class="todo-checkbox">
1295
+ <span class="checkbox-icon ${statusClass}">${statusIcon}</span>
1296
+ </div>
1297
+ <div class="todo-text">
1298
+ <span class="todo-content">${this.escapeHtml(displayText)}</span>
1299
+ <span class="todo-status-badge ${statusClass}">${todo.status.replace('_', ' ')}</span>
1300
+ </div>
1301
+ </div>
1302
+ `;
1303
+ });
1304
+
1305
+ html += `
1306
+ </div>
1307
+ </div>
1308
+ `;
1309
+ } else {
1310
+ html += `
1311
+ <div class="detail-section">
1312
+ <div class="no-todos">No todo items found</div>
1313
+ </div>
1314
+ `;
1315
+ }
1316
+
1317
+ // Add raw JSON section at the bottom
1318
+ html += `
1319
+ <div class="detail-section">
1320
+ <span class="detail-section-title">Parameters (${Object.keys(item.params).length})</span>
1321
+ <div class="params-list">
1322
+ <div class="param-item">
1323
+ <div class="param-key">todos:</div>
1324
+ <div class="param-value">
1325
+ <pre class="param-json">${this.escapeHtml(JSON.stringify(todos, null, 2))}</pre>
1326
+ </div>
1327
+ </div>
1328
+ </div>
1329
+ </div>
1330
+ `;
1094
1331
 
1095
- const now = new Date();
1096
- const diff = now - timestamp;
1097
- const minutes = diff / (1000 * 60);
1332
+ html += '</div>';
1333
+
1334
+ // Set the content directly
1335
+ const container = document.getElementById('module-data-content');
1336
+ if (container) {
1337
+ container.innerHTML = html;
1338
+ }
1098
1339
 
1099
- switch (this.timeRange) {
1100
- case '10min': return minutes <= 10;
1101
- case '30min': return minutes <= 30;
1102
- case 'hour': return minutes <= 60;
1103
- default: return true;
1340
+ // Update module header
1341
+ const moduleHeader = document.querySelector('.module-data-header h5');
1342
+ if (moduleHeader) {
1343
+ moduleHeader.textContent = `📝 tool: TodoWrite`;
1104
1344
  }
1105
1345
  }
1106
1346
 
1107
- /**
1108
- * Filter events by time
1109
- */
1110
- filterEventsByTime() {
1111
- this.initializeTreeData();
1347
+ // Utility methods for TodoWrite display
1348
+ formatStatus(status) {
1349
+ if (!status) return 'unknown';
1350
+
1351
+ const statusMap = {
1352
+ 'active': '🟢 Active',
1353
+ 'completed': '✅ Completed',
1354
+ 'in_progress': '🔄 In Progress',
1355
+ 'pending': '⏳ Pending',
1356
+ 'error': '❌ Error',
1357
+ 'failed': '❌ Failed'
1358
+ };
1112
1359
 
1113
- // Reprocess all events with new time filter
1114
- if (window.eventViewer && window.eventViewer.events) {
1115
- window.eventViewer.events.forEach(event => {
1116
- this.processEvent(event);
1117
- });
1360
+ return statusMap[status] || status;
1361
+ }
1362
+
1363
+ formatStatusClass(status) {
1364
+ return `status-${status}`;
1365
+ }
1366
+
1367
+ formatTimestamp(timestamp) {
1368
+ if (!timestamp) return '';
1369
+
1370
+ try {
1371
+ const date = new Date(timestamp);
1372
+ if (isNaN(date.getTime())) return '';
1373
+ return date.toLocaleTimeString();
1374
+ } catch (error) {
1375
+ return '';
1118
1376
  }
1119
1377
  }
1120
1378
 
1121
1379
  /**
1122
- * Update statistics
1380
+ * Display agent data in a simple format
1123
1381
  */
1124
- updateStats() {
1125
- // Check if we have a valid root node
1126
- if (!this.root || !this.root.data) {
1127
- console.warn('ActivityTree: Cannot update stats - root not initialized');
1128
- // Set default values
1129
- const nodeCountEl = document.getElementById('node-count');
1130
- const activeCountEl = document.getElementById('active-count');
1131
- const depthEl = document.getElementById('tree-depth');
1382
+ displayAgentData(agent) {
1383
+ const timestamp = this.formatTimestamp(agent.timestamp);
1384
+ const container = document.getElementById('module-data-content');
1385
+ if (!container) return;
1386
+
1387
+ let html = `
1388
+ <div class="detail-section">
1389
+ <span class="detail-section-title">Agent Information</span>
1390
+ <div class="agent-info">
1391
+ <div class="info-item">
1392
+ <span class="info-label">Name:</span>
1393
+ <span class="info-value">${this.escapeHtml(agent.name)}</span>
1394
+ </div>
1395
+ <div class="info-item">
1396
+ <span class="info-label">Status:</span>
1397
+ <span class="info-value status-${agent.status}">${agent.status}</span>
1398
+ </div>
1399
+ <div class="info-item">
1400
+ <span class="info-label">Timestamp:</span>
1401
+ <span class="info-value">${timestamp}</span>
1402
+ </div>
1403
+ <div class="info-item">
1404
+ <span class="info-label">Session ID:</span>
1405
+ <span class="info-value">${agent.sessionId || 'N/A'}</span>
1406
+ </div>
1407
+ </div>
1408
+ </div>
1409
+ `;
1410
+
1411
+ if (agent.tools && agent.tools.length > 0) {
1412
+ html += `
1413
+ <div class="detail-section">
1414
+ <span class="detail-section-title">Tools (${agent.tools.length})</span>
1415
+ <div class="tool-list">
1416
+ `;
1132
1417
 
1133
- if (nodeCountEl) nodeCountEl.textContent = '1';
1134
- if (activeCountEl) activeCountEl.textContent = '0';
1135
- if (depthEl) depthEl.textContent = '0';
1136
- return;
1418
+ agent.tools.forEach(tool => {
1419
+ html += `
1420
+ <div class="tool-item">
1421
+ <span class="tool-name">${this.escapeHtml(tool.name)}</span>
1422
+ <span class="tool-status status-${tool.status}">${tool.status}</span>
1423
+ </div>
1424
+ `;
1425
+ });
1426
+
1427
+ html += `
1428
+ </div>
1429
+ </div>
1430
+ `;
1137
1431
  }
1138
-
1139
- const nodeCount = this.countNodes(this.root);
1140
- const activeCount = this.countActiveNodes(this.root.data);
1141
- const depth = this.getTreeDepth(this.root);
1142
1432
 
1143
- const nodeCountEl = document.getElementById('node-count');
1144
- const activeCountEl = document.getElementById('active-count');
1145
- const depthEl = document.getElementById('tree-depth');
1146
-
1147
- if (nodeCountEl) nodeCountEl.textContent = nodeCount;
1148
- if (activeCountEl) activeCountEl.textContent = activeCount;
1149
- if (depthEl) depthEl.textContent = depth;
1150
-
1151
- console.log(`ActivityTree: Stats updated - Nodes: ${nodeCount}, Active: ${activeCount}, Depth: ${depth}`);
1433
+ container.innerHTML = html;
1152
1434
  }
1153
1435
 
1154
1436
  /**
1155
- * Count total nodes
1437
+ * Display tool data in a simple format
1156
1438
  */
1157
- countNodes(node) {
1158
- let count = 1;
1159
- if (node.children) {
1160
- node.children.forEach(child => {
1161
- count += this.countNodes(child);
1162
- });
1163
- }
1164
- if (node._children) {
1165
- node._children.forEach(child => {
1166
- count += this.countNodes(child);
1439
+ displayToolData(tool) {
1440
+ const timestamp = this.formatTimestamp(tool.timestamp);
1441
+ const container = document.getElementById('module-data-content');
1442
+ if (!container) return;
1443
+
1444
+ let html = `
1445
+ <div class="detail-section">
1446
+ <span class="detail-section-title">Tool Information</span>
1447
+ <div class="tool-info">
1448
+ <div class="info-item">
1449
+ <span class="info-label">Name:</span>
1450
+ <span class="info-value">${this.escapeHtml(tool.name)}</span>
1451
+ </div>
1452
+ <div class="info-item">
1453
+ <span class="info-label">Status:</span>
1454
+ <span class="info-value status-${tool.status}">${tool.status}</span>
1455
+ </div>
1456
+ <div class="info-item">
1457
+ <span class="info-label">Timestamp:</span>
1458
+ <span class="info-value">${timestamp}</span>
1459
+ </div>
1460
+ </div>
1461
+ </div>
1462
+ `;
1463
+
1464
+ if (tool.params && Object.keys(tool.params).length > 0) {
1465
+ html += `
1466
+ <div class="detail-section">
1467
+ <span class="detail-section-title">Parameters</span>
1468
+ <div class="params-list">
1469
+ `;
1470
+
1471
+ Object.entries(tool.params).forEach(([key, value]) => {
1472
+ html += `
1473
+ <div class="param-item">
1474
+ <div class="param-key">${this.escapeHtml(key)}:</div>
1475
+ <div class="param-value">${this.escapeHtml(String(value))}</div>
1476
+ </div>
1477
+ `;
1167
1478
  });
1479
+
1480
+ html += `
1481
+ </div>
1482
+ </div>
1483
+ `;
1168
1484
  }
1169
- return count;
1485
+
1486
+ container.innerHTML = html;
1170
1487
  }
1171
1488
 
1172
1489
  /**
1173
- * Count active nodes
1490
+ * Display instruction data in a simple format
1174
1491
  */
1175
- countActiveNodes(node) {
1176
- let count = node.status === 'in_progress' ? 1 : 0;
1177
- if (node.children) {
1178
- node.children.forEach(child => {
1179
- count += this.countActiveNodes(child);
1180
- });
1181
- }
1182
- if (node._children) {
1183
- node._children.forEach(child => {
1184
- count += this.countActiveNodes(child);
1185
- });
1186
- }
1187
- return count;
1492
+ displayInstructionData(instruction) {
1493
+ const timestamp = this.formatTimestamp(instruction.timestamp);
1494
+ const container = document.getElementById('module-data-content');
1495
+ if (!container) return;
1496
+
1497
+ const html = `
1498
+ <div class="detail-section">
1499
+ <span class="detail-section-title">User Instruction</span>
1500
+ <div class="instruction-info">
1501
+ <div class="info-item">
1502
+ <span class="info-label">Timestamp:</span>
1503
+ <span class="info-value">${timestamp}</span>
1504
+ </div>
1505
+ <div class="instruction-content">
1506
+ <div class="instruction-text">${this.escapeHtml(instruction.text)}</div>
1507
+ </div>
1508
+ </div>
1509
+ </div>
1510
+ `;
1511
+
1512
+ container.innerHTML = html;
1188
1513
  }
1189
1514
 
1190
1515
  /**
1191
- * Get tree depth
1516
+ * Display generic data for unknown types
1192
1517
  */
1193
- getTreeDepth(node) {
1194
- if (!node.children && !node._children) return 0;
1195
-
1196
- const children = node.children || node._children;
1197
- const depths = children.map(child => this.getTreeDepth(child));
1198
- return Math.max(...depths) + 1;
1518
+ displayGenericData(item, itemType) {
1519
+ const container = document.getElementById('module-data-content');
1520
+ if (!container) return;
1521
+
1522
+ let html = `
1523
+ <div class="detail-section">
1524
+ <span class="detail-section-title">${itemType || 'Item'} Data</span>
1525
+ <div class="generic-data">
1526
+ <pre>${this.escapeHtml(JSON.stringify(item, null, 2))}</pre>
1527
+ </div>
1528
+ </div>
1529
+ `;
1530
+
1531
+ container.innerHTML = html;
1199
1532
  }
1200
1533
 
1201
1534
  /**
1202
- * Update breadcrumb
1535
+ * Escape HTML for safe display
1203
1536
  */
1204
- updateBreadcrumb(node) {
1205
- const path = [];
1206
- let current = node;
1207
-
1208
- while (current) {
1209
- path.unshift(current.data.name);
1210
- current = current.parent;
1211
- }
1212
-
1213
- const breadcrumb = document.getElementById('activity-breadcrumb');
1214
- if (breadcrumb) {
1215
- breadcrumb.textContent = path.join(' > ');
1216
- }
1537
+ escapeHtml(text) {
1538
+ const div = document.createElement('div');
1539
+ div.textContent = text;
1540
+ return div.innerHTML;
1217
1541
  }
1218
1542
 
1219
1543
  /**
1220
- * Highlight search results
1544
+ * Reset zoom and pan to initial state
1221
1545
  */
1222
- highlightSearchResults() {
1223
- // Clear previous highlights
1224
- this.treeGroup.selectAll('.node-label')
1225
- .style('font-weight', 'normal')
1226
- .style('fill', '#2d3748');
1227
-
1228
- if (!this.searchTerm) return;
1229
-
1230
- // Highlight matching nodes
1231
- this.treeGroup.selectAll('.node-label')
1232
- .style('font-weight', d => {
1233
- return d.data.name.toLowerCase().includes(this.searchTerm) ? 'bold' : 'normal';
1234
- })
1235
- .style('fill', d => {
1236
- return d.data.name.toLowerCase().includes(this.searchTerm) ? '#e53e3e' : '#2d3748';
1237
- });
1546
+ resetZoom() {
1547
+ if (this.svg && this.zoom) {
1548
+ this.svg.transition()
1549
+ .duration(this.duration)
1550
+ .call(this.zoom.transform, d3.zoomIdentity);
1551
+ }
1552
+ }
1553
+
1554
+ /**
1555
+ * Escape JSON for safe inclusion in HTML attributes
1556
+ */
1557
+ escapeJson(obj) {
1558
+ return JSON.stringify(obj).replace(/'/g, '&apos;').replace(/"/g, '&quot;');
1238
1559
  }
1239
1560
  }
1240
1561
 
1241
- // Make ActivityTree globally available immediately when module loads
1562
+ // Make ActivityTree globally available
1242
1563
  window.ActivityTree = ActivityTree;
1243
1564
 
1244
1565
  // Initialize when the Activity tab is selected
1245
- // Only set up event listeners when DOM is ready, but expose class immediately
1246
1566
  const setupActivityTreeListeners = () => {
1247
1567
  let activityTree = null;
1248
1568
 
1249
- // Function to initialize the tree
1250
1569
  const initializeActivityTree = () => {
1251
1570
  if (!activityTree) {
1252
1571
  console.log('Creating new Activity Tree instance...');
1253
1572
  activityTree = new ActivityTree();
1254
- // Store instance globally for dashboard access
1255
1573
  window.activityTreeInstance = activityTree;
1574
+ window.activityTree = () => activityTree; // For debugging
1256
1575
  }
1257
1576
 
1258
- // Ensure the container is ready and clear any text
1259
- const container = document.getElementById('activity-tree-container') || document.getElementById('activity-tree');
1260
- if (container && container.textContent && container.textContent.trim()) {
1261
- console.log('Clearing text from activity tree container before init:', container.textContent);
1262
- container.textContent = '';
1263
- }
1264
-
1265
- // Always try to initialize when tab becomes active, even if instance exists
1266
- // Small delay to ensure DOM is ready and tab is visible
1267
1577
  setTimeout(() => {
1268
1578
  console.log('Attempting to initialize Activity Tree visualization...');
1269
1579
  activityTree.initialize();
@@ -1278,11 +1588,9 @@ const setupActivityTreeListeners = () => {
1278
1588
  if (tabName === 'activity') {
1279
1589
  console.log('Activity tab button clicked, initializing tree...');
1280
1590
  initializeActivityTree();
1281
- // Also call renderWhenVisible and forceShow to ensure proper rendering
1282
1591
  if (activityTree) {
1283
1592
  setTimeout(() => {
1284
1593
  activityTree.renderWhenVisible();
1285
- // Force show to ensure SVG is visible
1286
1594
  activityTree.forceShow();
1287
1595
  }, 150);
1288
1596
  }
@@ -1290,16 +1598,14 @@ const setupActivityTreeListeners = () => {
1290
1598
  });
1291
1599
  });
1292
1600
 
1293
- // Also listen for custom tab change events
1601
+ // Listen for custom tab change events
1294
1602
  document.addEventListener('tabChanged', (e) => {
1295
1603
  if (e.detail && e.detail.newTab === 'activity') {
1296
1604
  console.log('Tab changed to activity, initializing tree...');
1297
1605
  initializeActivityTree();
1298
- // Also call renderWhenVisible and forceShow to ensure proper rendering
1299
1606
  if (activityTree) {
1300
1607
  setTimeout(() => {
1301
1608
  activityTree.renderWhenVisible();
1302
- // Force show to ensure SVG is visible
1303
1609
  activityTree.forceShow();
1304
1610
  }, 150);
1305
1611
  }
@@ -1313,7 +1619,6 @@ const setupActivityTreeListeners = () => {
1313
1619
  initializeActivityTree();
1314
1620
  }
1315
1621
 
1316
- // Also check the tab panel directly
1317
1622
  const activityPanel = document.getElementById('activity-tab');
1318
1623
  if (activityPanel && activityPanel.classList.contains('active')) {
1319
1624
  console.log('Activity panel is active on load, initializing tree...');
@@ -1321,16 +1626,12 @@ const setupActivityTreeListeners = () => {
1321
1626
  initializeActivityTree();
1322
1627
  }
1323
1628
  }
1324
-
1325
- // Export for debugging
1326
- window.activityTree = () => activityTree; // Expose instance getter for debugging
1327
1629
  };
1328
1630
 
1329
1631
  // Set up listeners when DOM is ready
1330
1632
  if (document.readyState === 'loading') {
1331
1633
  document.addEventListener('DOMContentLoaded', setupActivityTreeListeners);
1332
1634
  } else {
1333
- // DOM already loaded
1334
1635
  setupActivityTreeListeners();
1335
1636
  }
1336
1637