claude-code-templates 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/hook-scanner.js +21 -1
- package/src/index.js +24 -1
- package/src/templates.js +28 -0
- package/src/test-console-bridge.js +67 -0
- package/src/utils.js +46 -0
- package/templates/ruby/.claude/commands/model.md +360 -0
- package/templates/ruby/.claude/commands/test.md +480 -0
- package/templates/ruby/.claude/settings.json +146 -0
- package/templates/ruby/.mcp.json +83 -0
- package/templates/ruby/CLAUDE.md +284 -0
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +490 -0
- package/templates/ruby/examples/rails-app/CLAUDE.md +376 -0
|
@@ -0,0 +1,2535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentsPage - Dedicated page for managing and viewing agent conversations
|
|
3
|
+
* Handles conversation display, filtering, and detailed analysis
|
|
4
|
+
*/
|
|
5
|
+
class AgentsPage {
|
|
6
|
+
constructor(container, services) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.dataService = services.data;
|
|
9
|
+
this.stateService = services.state;
|
|
10
|
+
|
|
11
|
+
this.components = {};
|
|
12
|
+
this.filters = {
|
|
13
|
+
status: 'all',
|
|
14
|
+
timeRange: '7d',
|
|
15
|
+
search: ''
|
|
16
|
+
};
|
|
17
|
+
this.isInitialized = false;
|
|
18
|
+
|
|
19
|
+
// Pagination state for conversations
|
|
20
|
+
this.pagination = {
|
|
21
|
+
currentPage: 0,
|
|
22
|
+
limit: 10,
|
|
23
|
+
hasMore: true,
|
|
24
|
+
isLoading: false
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Pagination state for messages
|
|
28
|
+
this.messagesPagination = {
|
|
29
|
+
currentPage: 0,
|
|
30
|
+
limit: 10,
|
|
31
|
+
hasMore: true,
|
|
32
|
+
isLoading: false,
|
|
33
|
+
conversationId: null
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Loaded conversations cache
|
|
37
|
+
this.loadedConversations = [];
|
|
38
|
+
this.loadedMessages = new Map(); // Cache messages by conversation ID (now stores paginated data)
|
|
39
|
+
|
|
40
|
+
// State transition tracking for enhanced user experience
|
|
41
|
+
this.lastMessageTime = new Map(); // Track when last message was received per conversation
|
|
42
|
+
|
|
43
|
+
// Initialize tool display component
|
|
44
|
+
this.toolDisplay = new ToolDisplay();
|
|
45
|
+
|
|
46
|
+
// Subscribe to state changes
|
|
47
|
+
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
|
|
48
|
+
|
|
49
|
+
// Subscribe to DataService events for real-time updates
|
|
50
|
+
this.dataService.addEventListener((type, data) => {
|
|
51
|
+
if (type === 'new_message') {
|
|
52
|
+
console.log('🔄 WebSocket: New message received', { conversationId: data.conversationId });
|
|
53
|
+
this.handleNewMessage(data.conversationId, data.message, data.metadata);
|
|
54
|
+
} else if (type === 'console_interaction') {
|
|
55
|
+
console.log('🔄 WebSocket: Console interaction request received', data);
|
|
56
|
+
this.showConsoleInteraction(data);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the agents page
|
|
63
|
+
*/
|
|
64
|
+
async initialize() {
|
|
65
|
+
if (this.isInitialized) return;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.stateService.setLoading(true);
|
|
69
|
+
await this.render();
|
|
70
|
+
await this.initializeComponents();
|
|
71
|
+
await this.loadConversationsData();
|
|
72
|
+
this.isInitialized = true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error initializing agents page:', error);
|
|
75
|
+
this.stateService.setError(error);
|
|
76
|
+
} finally {
|
|
77
|
+
this.stateService.setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handle state changes from StateService (WebSocket updates)
|
|
83
|
+
* @param {Object} state - New state
|
|
84
|
+
* @param {string} action - Action that caused the change
|
|
85
|
+
*/
|
|
86
|
+
handleStateChange(state, action) {
|
|
87
|
+
switch (action) {
|
|
88
|
+
case 'update_conversations':
|
|
89
|
+
// Don't replace loaded conversations, just update states
|
|
90
|
+
break;
|
|
91
|
+
case 'update_conversation_states':
|
|
92
|
+
console.log('🔄 WebSocket: Conversation states updated', { count: Object.keys(state.conversationStates?.activeStates || state.conversationStates || {}).length });
|
|
93
|
+
|
|
94
|
+
// Handle both direct states object and nested structure
|
|
95
|
+
const activeStates = state.conversationStates?.activeStates || state.conversationStates || {};
|
|
96
|
+
|
|
97
|
+
this.updateConversationStates(activeStates);
|
|
98
|
+
break;
|
|
99
|
+
case 'set_loading':
|
|
100
|
+
this.updateLoadingState(state.isLoading);
|
|
101
|
+
break;
|
|
102
|
+
case 'set_error':
|
|
103
|
+
this.updateErrorState(state.error);
|
|
104
|
+
break;
|
|
105
|
+
case 'conversation_state_change':
|
|
106
|
+
this.handleConversationStateChange(state);
|
|
107
|
+
break;
|
|
108
|
+
case 'data_refresh':
|
|
109
|
+
// On real-time data refresh, update conversation states but keep pagination
|
|
110
|
+
this.updateConversationStatesOnly();
|
|
111
|
+
break;
|
|
112
|
+
case 'new_message':
|
|
113
|
+
// Handle new message in real-time
|
|
114
|
+
this.handleNewMessage(state.conversationId, state.message, state.metadata);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle new message received via WebSocket
|
|
121
|
+
* @param {string} conversationId - Conversation ID that received new message
|
|
122
|
+
* @param {Object} message - New message object
|
|
123
|
+
* @param {Object} metadata - Additional metadata
|
|
124
|
+
*/
|
|
125
|
+
handleNewMessage(conversationId, message, metadata) {
|
|
126
|
+
// Log essential message info for debugging
|
|
127
|
+
console.log('🔄 WebSocket: Processing new message', {
|
|
128
|
+
conversationId,
|
|
129
|
+
role: message?.role,
|
|
130
|
+
hasTools: Array.isArray(message?.content) ? message.content.some(b => b.type === 'tool_use') : false,
|
|
131
|
+
hasToolResults: !!message?.toolResults
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Always update the message cache for this conversation
|
|
135
|
+
const existingMessages = this.loadedMessages.get(conversationId) || [];
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// Track message timing for better state transitions
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
this.lastMessageTime.set(conversationId, now);
|
|
141
|
+
|
|
142
|
+
// IMMEDIATE STATE TRANSITION based on message appearance
|
|
143
|
+
if (this.selectedConversationId === conversationId) {
|
|
144
|
+
if (message?.role === 'user') {
|
|
145
|
+
// User message just appeared - Claude immediately starts working
|
|
146
|
+
console.log('⚡ User message detected - Claude starting work immediately');
|
|
147
|
+
this.updateStateBanner(conversationId, 'Claude Code working...');
|
|
148
|
+
} else if (message?.role === 'assistant') {
|
|
149
|
+
// Assistant message appeared - analyze for specific state
|
|
150
|
+
const intelligentState = this.analyzeMessageForState(message, existingMessages);
|
|
151
|
+
console.log(`🤖 Assistant message detected - state: ${intelligentState}`);
|
|
152
|
+
this.updateStateBanner(conversationId, intelligentState);
|
|
153
|
+
|
|
154
|
+
// No additional timeout needed - state is determined by message content
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if we already have this message (avoid duplicates)
|
|
159
|
+
const messageExists = existingMessages.some(msg =>
|
|
160
|
+
msg.id === message.id ||
|
|
161
|
+
(msg.timestamp === message.timestamp && msg.role === message.role)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!messageExists) {
|
|
165
|
+
// Add new message to the end
|
|
166
|
+
const updatedMessages = [...existingMessages, message];
|
|
167
|
+
this.loadedMessages.set(conversationId, updatedMessages);
|
|
168
|
+
|
|
169
|
+
// Refresh only the conversation states to show updated status/timestamp
|
|
170
|
+
// Don't do full reload as it can interfere with message cache
|
|
171
|
+
this.updateConversationStatesOnly();
|
|
172
|
+
|
|
173
|
+
// If this conversation is currently selected, update the messages view
|
|
174
|
+
if (this.selectedConversationId === conversationId) {
|
|
175
|
+
// Re-render messages with new message
|
|
176
|
+
this.renderCachedMessages(updatedMessages, false);
|
|
177
|
+
|
|
178
|
+
// Auto-scroll to new message
|
|
179
|
+
this.scrollToBottom();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Show notification
|
|
183
|
+
this.showNewMessageNotification(message, metadata);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Update only conversation states without affecting pagination
|
|
189
|
+
*/
|
|
190
|
+
async updateConversationStatesOnly() {
|
|
191
|
+
try {
|
|
192
|
+
const statesData = await this.dataService.getConversationStates();
|
|
193
|
+
const activeStates = statesData?.activeStates || {};
|
|
194
|
+
|
|
195
|
+
// Update StateService with fresh states
|
|
196
|
+
this.stateService.updateConversationStates(activeStates);
|
|
197
|
+
|
|
198
|
+
// Update states in already loaded conversations
|
|
199
|
+
this.updateConversationStateElements(activeStates);
|
|
200
|
+
|
|
201
|
+
// Update banner if we have a selected conversation
|
|
202
|
+
if (this.selectedConversationId && activeStates[this.selectedConversationId]) {
|
|
203
|
+
this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error updating conversation states:', error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Analyze a message to determine intelligent conversation state
|
|
213
|
+
* @param {Object} message - The message to analyze
|
|
214
|
+
* @param {Array} existingMessages - Previous messages in conversation
|
|
215
|
+
* @returns {string} Intelligent state description
|
|
216
|
+
*/
|
|
217
|
+
analyzeMessageForState(message, existingMessages = []) {
|
|
218
|
+
const role = message?.role;
|
|
219
|
+
const content = message?.content;
|
|
220
|
+
const hasToolResults = !!message?.toolResults && message.toolResults.length > 0;
|
|
221
|
+
const messageTime = new Date(message?.timestamp || Date.now());
|
|
222
|
+
const now = new Date();
|
|
223
|
+
const messageAge = (now - messageTime) / 1000; // seconds
|
|
224
|
+
|
|
225
|
+
if (role === 'assistant') {
|
|
226
|
+
// Analyze assistant messages with enhanced logic
|
|
227
|
+
if (Array.isArray(content)) {
|
|
228
|
+
const hasToolUse = content.some(block => block.type === 'tool_use');
|
|
229
|
+
const hasText = content.some(block => block.type === 'text');
|
|
230
|
+
const textBlocks = content.filter(block => block.type === 'text');
|
|
231
|
+
const toolUseBlocks = content.filter(block => block.type === 'tool_use');
|
|
232
|
+
|
|
233
|
+
// Enhanced tool execution detection with immediate response
|
|
234
|
+
if (hasToolUse) {
|
|
235
|
+
const toolNames = toolUseBlocks.map(tool => tool.name).join(', ');
|
|
236
|
+
|
|
237
|
+
if (!hasToolResults) {
|
|
238
|
+
// Tool just sent - immediate execution state
|
|
239
|
+
console.log(`🔧 Tools detected: ${toolNames} - showing execution state`);
|
|
240
|
+
|
|
241
|
+
if (toolNames.includes('bash') || toolNames.includes('edit') || toolNames.includes('write') || toolNames.includes('multiedit')) {
|
|
242
|
+
return 'Executing tools...';
|
|
243
|
+
} else if (toolNames.includes('read') || toolNames.includes('grep') || toolNames.includes('glob') || toolNames.includes('task')) {
|
|
244
|
+
return 'Analyzing code...';
|
|
245
|
+
} else if (toolNames.includes('webfetch') || toolNames.includes('websearch')) {
|
|
246
|
+
return 'Fetching data...';
|
|
247
|
+
}
|
|
248
|
+
return 'Awaiting tool response...';
|
|
249
|
+
} else {
|
|
250
|
+
// Has tool results - Claude is processing them
|
|
251
|
+
console.log(`📊 Tools completed: ${toolNames} - analyzing results`);
|
|
252
|
+
return 'Analyzing results...';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Enhanced text analysis
|
|
257
|
+
if (hasText) {
|
|
258
|
+
const textContent = textBlocks.map(block => block.text).join(' ').toLowerCase();
|
|
259
|
+
|
|
260
|
+
// Working indicators
|
|
261
|
+
if (textContent.includes('let me') ||
|
|
262
|
+
textContent.includes('i\'ll') ||
|
|
263
|
+
textContent.includes('i will') ||
|
|
264
|
+
textContent.includes('i\'m going to') ||
|
|
265
|
+
textContent.includes('let\'s') ||
|
|
266
|
+
textContent.includes('first, i\'ll') ||
|
|
267
|
+
textContent.includes('now i\'ll')) {
|
|
268
|
+
return 'Claude Code working...';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Analysis indicators
|
|
272
|
+
if (textContent.includes('analyzing') ||
|
|
273
|
+
textContent.includes('examining') ||
|
|
274
|
+
textContent.includes('looking at') ||
|
|
275
|
+
textContent.includes('reviewing')) {
|
|
276
|
+
return 'Analyzing code...';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Completion indicators
|
|
280
|
+
if (textContent.includes('completed') ||
|
|
281
|
+
textContent.includes('finished') ||
|
|
282
|
+
textContent.includes('done') ||
|
|
283
|
+
textContent.includes('successfully')) {
|
|
284
|
+
return 'Task completed';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// User input needed - enhanced detection
|
|
288
|
+
if (textContent.endsWith('?') ||
|
|
289
|
+
textContent.includes('what would you like') ||
|
|
290
|
+
textContent.includes('how can i help') ||
|
|
291
|
+
textContent.includes('would you like me to') ||
|
|
292
|
+
textContent.includes('should i') ||
|
|
293
|
+
textContent.includes('do you want') ||
|
|
294
|
+
textContent.includes('let me know') ||
|
|
295
|
+
textContent.includes('please let me know') ||
|
|
296
|
+
textContent.includes('what do you think') ||
|
|
297
|
+
textContent.includes('any questions')) {
|
|
298
|
+
return 'Waiting for your response';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Error/problem indicators
|
|
302
|
+
if (textContent.includes('error') ||
|
|
303
|
+
textContent.includes('failed') ||
|
|
304
|
+
textContent.includes('problem') ||
|
|
305
|
+
textContent.includes('issue')) {
|
|
306
|
+
return 'Encountered issue';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Recent assistant message suggests waiting for user
|
|
312
|
+
if (messageAge < 300) { // Extended to 5 minutes
|
|
313
|
+
return 'Waiting for your response';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Default for older assistant messages
|
|
317
|
+
return 'Idle';
|
|
318
|
+
|
|
319
|
+
} else if (role === 'user') {
|
|
320
|
+
// User just sent a message - Claude should be processing
|
|
321
|
+
if (messageAge < 10) {
|
|
322
|
+
return 'Claude Code working...';
|
|
323
|
+
} else if (messageAge < 60) {
|
|
324
|
+
return 'Awaiting response...';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Older user messages suggest Claude might be working on something complex
|
|
328
|
+
return 'Processing request...';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Enhanced timing analysis
|
|
332
|
+
const lastMessage = existingMessages[existingMessages.length - 1];
|
|
333
|
+
if (lastMessage) {
|
|
334
|
+
const timeSinceLastMessage = Date.now() - new Date(lastMessage.timestamp).getTime();
|
|
335
|
+
|
|
336
|
+
if (timeSinceLastMessage < 30000) { // Less than 30 seconds
|
|
337
|
+
return lastMessage.role === 'user' ? 'Claude Code working...' : 'Recently active';
|
|
338
|
+
} else if (timeSinceLastMessage < 180000) { // Less than 3 minutes
|
|
339
|
+
return 'Idle';
|
|
340
|
+
} else if (timeSinceLastMessage < 1800000) { // Less than 30 minutes
|
|
341
|
+
return 'Waiting for your response';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return 'Inactive';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Show console interaction panel for Yes/No prompts
|
|
351
|
+
* @param {Object} interactionData - Interaction data from Claude Code
|
|
352
|
+
*/
|
|
353
|
+
showConsoleInteraction(interactionData) {
|
|
354
|
+
const panel = this.container.querySelector('#console-interaction-panel');
|
|
355
|
+
const description = this.container.querySelector('#interaction-description');
|
|
356
|
+
const prompt = this.container.querySelector('#interaction-prompt');
|
|
357
|
+
const choices = this.container.querySelector('#interaction-choices');
|
|
358
|
+
const textInput = this.container.querySelector('#interaction-text-input');
|
|
359
|
+
|
|
360
|
+
// Show the panel
|
|
361
|
+
panel.style.display = 'block';
|
|
362
|
+
|
|
363
|
+
// Set up the interaction content
|
|
364
|
+
if (interactionData.description) {
|
|
365
|
+
description.innerHTML = `
|
|
366
|
+
<div class="tool-action">
|
|
367
|
+
<strong>${interactionData.tool || 'Action'}:</strong>
|
|
368
|
+
<div class="tool-details">${interactionData.description}</div>
|
|
369
|
+
</div>
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (interactionData.prompt) {
|
|
374
|
+
prompt.textContent = interactionData.prompt;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Handle different interaction types
|
|
378
|
+
if (interactionData.type === 'choice' && interactionData.options) {
|
|
379
|
+
// Show multiple choice options
|
|
380
|
+
choices.style.display = 'block';
|
|
381
|
+
textInput.style.display = 'none';
|
|
382
|
+
|
|
383
|
+
const choicesHtml = interactionData.options.map((option, index) => `
|
|
384
|
+
<label class="interaction-choice">
|
|
385
|
+
<input type="radio" name="console-choice" value="${index}" ${index === 0 ? 'checked' : ''}>
|
|
386
|
+
<span class="choice-number">${index + 1}.</span>
|
|
387
|
+
<span class="choice-text">${option}</span>
|
|
388
|
+
</label>
|
|
389
|
+
`).join('');
|
|
390
|
+
|
|
391
|
+
choices.innerHTML = choicesHtml;
|
|
392
|
+
|
|
393
|
+
} else if (interactionData.type === 'text') {
|
|
394
|
+
// Show text input
|
|
395
|
+
choices.style.display = 'none';
|
|
396
|
+
textInput.style.display = 'block';
|
|
397
|
+
|
|
398
|
+
const textarea = this.container.querySelector('#console-text-input');
|
|
399
|
+
textarea.focus();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Store interaction data for submission
|
|
403
|
+
this.currentInteraction = interactionData;
|
|
404
|
+
|
|
405
|
+
// Bind event listeners
|
|
406
|
+
this.bindInteractionEvents();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Hide console interaction panel
|
|
411
|
+
*/
|
|
412
|
+
hideConsoleInteraction() {
|
|
413
|
+
const panel = this.container.querySelector('#console-interaction-panel');
|
|
414
|
+
panel.style.display = 'none';
|
|
415
|
+
this.currentInteraction = null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Bind event listeners for console interaction
|
|
420
|
+
*/
|
|
421
|
+
bindInteractionEvents() {
|
|
422
|
+
const submitBtn = this.container.querySelector('#interaction-submit');
|
|
423
|
+
const cancelBtn = this.container.querySelector('#interaction-cancel');
|
|
424
|
+
|
|
425
|
+
// Remove existing listeners
|
|
426
|
+
submitBtn.replaceWith(submitBtn.cloneNode(true));
|
|
427
|
+
cancelBtn.replaceWith(cancelBtn.cloneNode(true));
|
|
428
|
+
|
|
429
|
+
// Get fresh references
|
|
430
|
+
const newSubmitBtn = this.container.querySelector('#interaction-submit');
|
|
431
|
+
const newCancelBtn = this.container.querySelector('#interaction-cancel');
|
|
432
|
+
|
|
433
|
+
newSubmitBtn.addEventListener('click', () => this.handleInteractionSubmit());
|
|
434
|
+
newCancelBtn.addEventListener('click', () => this.handleInteractionCancel());
|
|
435
|
+
|
|
436
|
+
// Handle Enter key for text input
|
|
437
|
+
const textarea = this.container.querySelector('#console-text-input');
|
|
438
|
+
if (textarea) {
|
|
439
|
+
textarea.addEventListener('keydown', (e) => {
|
|
440
|
+
if (e.key === 'Enter' && e.ctrlKey) {
|
|
441
|
+
this.handleInteractionSubmit();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handle interaction submission
|
|
449
|
+
*/
|
|
450
|
+
async handleInteractionSubmit() {
|
|
451
|
+
if (!this.currentInteraction) return;
|
|
452
|
+
|
|
453
|
+
let response;
|
|
454
|
+
|
|
455
|
+
if (this.currentInteraction.type === 'choice') {
|
|
456
|
+
const selectedChoice = this.container.querySelector('input[name="console-choice"]:checked');
|
|
457
|
+
if (selectedChoice) {
|
|
458
|
+
response = {
|
|
459
|
+
type: 'choice',
|
|
460
|
+
value: parseInt(selectedChoice.value),
|
|
461
|
+
text: this.currentInteraction.options[selectedChoice.value]
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
} else if (this.currentInteraction.type === 'text') {
|
|
465
|
+
const textarea = this.container.querySelector('#console-text-input');
|
|
466
|
+
response = {
|
|
467
|
+
type: 'text',
|
|
468
|
+
value: textarea.value.trim()
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (response) {
|
|
473
|
+
// Send response via WebSocket
|
|
474
|
+
try {
|
|
475
|
+
await this.sendConsoleResponse(this.currentInteraction.id, response);
|
|
476
|
+
console.log('🔄 WebSocket: Console interaction response sent', { id: this.currentInteraction.id, response });
|
|
477
|
+
this.hideConsoleInteraction();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error('Error sending console response:', error);
|
|
480
|
+
// Show error in UI
|
|
481
|
+
this.showInteractionError('Failed to send response. Please try again.');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Handle interaction cancellation
|
|
488
|
+
*/
|
|
489
|
+
async handleInteractionCancel() {
|
|
490
|
+
if (!this.currentInteraction) return;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await this.sendConsoleResponse(this.currentInteraction.id, { type: 'cancel' });
|
|
494
|
+
console.log('🔄 WebSocket: Console interaction cancelled', { id: this.currentInteraction.id });
|
|
495
|
+
this.hideConsoleInteraction();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error('Error cancelling console interaction:', error);
|
|
498
|
+
this.hideConsoleInteraction(); // Hide anyway on cancel
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Send console response via WebSocket
|
|
504
|
+
* @param {string} interactionId - Interaction ID
|
|
505
|
+
* @param {Object} response - Response data
|
|
506
|
+
*/
|
|
507
|
+
async sendConsoleResponse(interactionId, response) {
|
|
508
|
+
// Send through DataService which will route to WebSocket
|
|
509
|
+
if (this.dataService && this.dataService.webSocketService) {
|
|
510
|
+
this.dataService.webSocketService.send({
|
|
511
|
+
type: 'console_response',
|
|
512
|
+
data: {
|
|
513
|
+
interactionId,
|
|
514
|
+
response
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
throw new Error('WebSocket service not available');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Show error in interaction panel
|
|
524
|
+
* @param {string} message - Error message
|
|
525
|
+
*/
|
|
526
|
+
showInteractionError(message) {
|
|
527
|
+
const panel = this.container.querySelector('#console-interaction-panel');
|
|
528
|
+
const existingError = panel.querySelector('.interaction-error');
|
|
529
|
+
|
|
530
|
+
if (existingError) {
|
|
531
|
+
existingError.remove();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const errorDiv = document.createElement('div');
|
|
535
|
+
errorDiv.className = 'interaction-error';
|
|
536
|
+
errorDiv.textContent = message;
|
|
537
|
+
|
|
538
|
+
const content = panel.querySelector('.interaction-content');
|
|
539
|
+
content.insertBefore(errorDiv, content.querySelector('.interaction-actions'));
|
|
540
|
+
|
|
541
|
+
// Remove error after 5 seconds
|
|
542
|
+
setTimeout(() => {
|
|
543
|
+
if (errorDiv.parentNode) {
|
|
544
|
+
errorDiv.remove();
|
|
545
|
+
}
|
|
546
|
+
}, 5000);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Test console interaction functionality (for development)
|
|
551
|
+
*/
|
|
552
|
+
testConsoleInteraction() {
|
|
553
|
+
// Test choice-based interaction (like your example)
|
|
554
|
+
const testChoiceInteraction = {
|
|
555
|
+
id: 'test-choice-' + Date.now(),
|
|
556
|
+
type: 'choice',
|
|
557
|
+
tool: 'Search',
|
|
558
|
+
description: 'Search(pattern: "(?:Yes|No|yes|no)(?:,\\s*and\\s*don\'t\\s*ask\\s*again)?", path: "../../../../../../../.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates", include: "*.jsonl")',
|
|
559
|
+
prompt: 'Do you want to proceed?',
|
|
560
|
+
options: [
|
|
561
|
+
'Yes',
|
|
562
|
+
'Yes, and add /Users/danipower/.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates as a working directory for this session',
|
|
563
|
+
'No, and tell Claude what to do differently'
|
|
564
|
+
]
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Test text input interaction
|
|
568
|
+
const testTextInteraction = {
|
|
569
|
+
id: 'test-text-' + Date.now(),
|
|
570
|
+
type: 'text',
|
|
571
|
+
tool: 'Console Input',
|
|
572
|
+
description: 'Claude Code is requesting text input from the console.',
|
|
573
|
+
prompt: 'Please provide your input:'
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Randomly choose which type to test, or ask user
|
|
577
|
+
const testType = Math.random() > 0.5 ? testChoiceInteraction : testTextInteraction;
|
|
578
|
+
|
|
579
|
+
console.log('🧪 Testing console interaction:', testType);
|
|
580
|
+
this.showConsoleInteraction(testType);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Update conversation state elements in the DOM
|
|
585
|
+
* @param {Object} activeStates - Active conversation states
|
|
586
|
+
*/
|
|
587
|
+
updateConversationStateElements(activeStates) {
|
|
588
|
+
const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
|
|
589
|
+
|
|
590
|
+
conversationItems.forEach(item => {
|
|
591
|
+
const conversationId = item.dataset.id;
|
|
592
|
+
const state = activeStates[conversationId] || 'unknown';
|
|
593
|
+
const stateClass = this.getStateClass(state);
|
|
594
|
+
const stateLabel = this.getStateLabel(state);
|
|
595
|
+
|
|
596
|
+
// Update status dot
|
|
597
|
+
const statusDot = item.querySelector('.status-dot');
|
|
598
|
+
if (statusDot) {
|
|
599
|
+
statusDot.className = `status-dot ${stateClass}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Update status badge
|
|
603
|
+
const statusBadge = item.querySelector('.sidebar-conversation-badge');
|
|
604
|
+
if (statusBadge) {
|
|
605
|
+
statusBadge.className = `sidebar-conversation-badge ${stateClass}`;
|
|
606
|
+
statusBadge.textContent = stateLabel;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Render the agents page structure
|
|
613
|
+
*/
|
|
614
|
+
async render() {
|
|
615
|
+
this.container.innerHTML = `
|
|
616
|
+
<div class="agents-page">
|
|
617
|
+
<!-- Page Header -->
|
|
618
|
+
<div class="page-header conversations-header">
|
|
619
|
+
<div class="header-content">
|
|
620
|
+
<div class="header-left">
|
|
621
|
+
<div class="status-header">
|
|
622
|
+
<span class="session-timer-status-dot active"></span>
|
|
623
|
+
<h1 class="page-title">
|
|
624
|
+
Claude Code web UI
|
|
625
|
+
</h1>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="page-subtitle">
|
|
628
|
+
Monitor and analyze Claude Code agent interactions in real-time
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<!-- Filters Section -->
|
|
635
|
+
<div class="conversations-filters">
|
|
636
|
+
<div class="filters-row">
|
|
637
|
+
<div class="filter-group">
|
|
638
|
+
<label class="filter-label">Status:</label>
|
|
639
|
+
<select class="filter-select" id="status-filter">
|
|
640
|
+
<option value="all">All</option>
|
|
641
|
+
<option value="active">Active</option>
|
|
642
|
+
<option value="inactive">Inactive</option>
|
|
643
|
+
</select>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<div class="filter-group">
|
|
647
|
+
<label class="filter-label">Time Range:</label>
|
|
648
|
+
<select class="filter-select" id="time-filter">
|
|
649
|
+
<option value="1h">Last Hour</option>
|
|
650
|
+
<option value="24h">Last 24 Hours</option>
|
|
651
|
+
<option value="7d" selected>Last 7 Days</option>
|
|
652
|
+
<option value="30d">Last 30 Days</option>
|
|
653
|
+
</select>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<div class="filter-group search-group">
|
|
657
|
+
<label class="filter-label">Search:</label>
|
|
658
|
+
<div class="search-input-container">
|
|
659
|
+
<input type="text" class="filter-input search-input" id="search-filter" placeholder="Search conversations, projects, or messages...">
|
|
660
|
+
<button class="search-clear" id="clear-search" title="Clear search">×</button>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
<!-- Loading State -->
|
|
667
|
+
<div class="loading-state" id="conversations-loading" style="display: none;">
|
|
668
|
+
<div class="loading-spinner"></div>
|
|
669
|
+
<span class="loading-text">Loading conversations...</span>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<!-- Error State -->
|
|
673
|
+
<div class="error-state" id="conversations-error" style="display: none;">
|
|
674
|
+
<div class="error-content">
|
|
675
|
+
<span class="error-icon">⚠️</span>
|
|
676
|
+
<span class="error-message"></span>
|
|
677
|
+
<button class="error-retry" id="retry-load">Retry</button>
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
|
|
681
|
+
<!-- Console Interaction Panel (Hidden by default) -->
|
|
682
|
+
<div id="console-interaction-panel" class="console-interaction-panel" style="display: none;">
|
|
683
|
+
<div class="interaction-header">
|
|
684
|
+
<div class="interaction-title">
|
|
685
|
+
<span class="interaction-icon">⚡</span>
|
|
686
|
+
<span class="interaction-text">Claude Code needs your input</span>
|
|
687
|
+
</div>
|
|
688
|
+
<button class="interaction-close" onclick="this.hideConsoleInteraction()">×</button>
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
<div class="interaction-content">
|
|
692
|
+
<div id="interaction-description" class="interaction-description">
|
|
693
|
+
<!-- Tool description will be inserted here -->
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div id="interaction-prompt" class="interaction-prompt">
|
|
697
|
+
Do you want to proceed?
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<!-- Multi-choice options -->
|
|
701
|
+
<div id="interaction-choices" class="interaction-choices" style="display: none;">
|
|
702
|
+
<!-- Radio button choices will be inserted here -->
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
<!-- Text input area -->
|
|
706
|
+
<div id="interaction-text-input" class="interaction-text-input" style="display: none;">
|
|
707
|
+
<label for="console-text-input">Your response:</label>
|
|
708
|
+
<textarea id="console-text-input" placeholder="Type your response here..." rows="4"></textarea>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div class="interaction-actions">
|
|
712
|
+
<button id="interaction-submit" class="interaction-btn primary">Submit</button>
|
|
713
|
+
<button id="interaction-cancel" class="interaction-btn secondary">Cancel</button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<!-- Two Column Layout -->
|
|
719
|
+
<div class="conversations-layout">
|
|
720
|
+
<!-- Left Sidebar: Conversations List -->
|
|
721
|
+
<div class="conversations-sidebar">
|
|
722
|
+
<div class="sidebar-header">
|
|
723
|
+
<h3>Chats</h3>
|
|
724
|
+
<span class="conversation-count" id="sidebar-count">0</span>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="conversations-list" id="conversations-list">
|
|
727
|
+
<!-- Conversation items will be rendered here -->
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<!-- Load More Indicator -->
|
|
731
|
+
<div class="load-more-indicator" id="load-more-indicator" style="display: none;">
|
|
732
|
+
<div class="loading-spinner"></div>
|
|
733
|
+
<span class="loading-text">Loading more conversations...</span>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<!-- Right Panel: Messages Detail -->
|
|
738
|
+
<div class="messages-panel">
|
|
739
|
+
<div class="messages-header" id="messages-header">
|
|
740
|
+
<div class="selected-conversation-info">
|
|
741
|
+
<h3 id="selected-conversation-title">Select a chat</h3>
|
|
742
|
+
<div class="selected-conversation-meta" id="selected-conversation-meta"></div>
|
|
743
|
+
</div>
|
|
744
|
+
<div class="messages-actions">
|
|
745
|
+
<button class="action-btn-small" id="export-conversation" title="Export conversation">
|
|
746
|
+
<span class="btn-icon-small">📁</span>
|
|
747
|
+
Export
|
|
748
|
+
</button>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
<div class="messages-content" id="messages-content">
|
|
753
|
+
<div class="no-conversation-selected">
|
|
754
|
+
<div class="no-selection-icon">💬</div>
|
|
755
|
+
<h4>No conversation selected</h4>
|
|
756
|
+
<p>Choose a conversation from the sidebar to view its messages</p>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<!-- Conversation State Banner -->
|
|
761
|
+
<div class="conversation-state-banner" id="conversation-state-banner" style="display: none;">
|
|
762
|
+
<div class="state-indicator">
|
|
763
|
+
<span class="state-dot" id="state-dot"></span>
|
|
764
|
+
<span class="state-text" id="state-text">Ready</span>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="state-timestamp" id="state-timestamp"></div>
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<!-- Empty State -->
|
|
772
|
+
<div class="empty-state" id="empty-state" style="display: none;">
|
|
773
|
+
<div class="empty-content">
|
|
774
|
+
<span class="empty-icon">💬</span>
|
|
775
|
+
<h3>No conversations found</h3>
|
|
776
|
+
<p>No agent conversations match your current filters.</p>
|
|
777
|
+
<button class="empty-action" id="clear-filters">Clear Filters</button>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
`;
|
|
782
|
+
|
|
783
|
+
this.bindEvents();
|
|
784
|
+
this.setupInfiniteScroll();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Initialize child components
|
|
789
|
+
*/
|
|
790
|
+
async initializeComponents() {
|
|
791
|
+
// Initialize ConversationTable for detailed view if available
|
|
792
|
+
const tableContainer = this.container.querySelector('#conversations-table');
|
|
793
|
+
if (tableContainer && typeof ConversationTable !== 'undefined') {
|
|
794
|
+
try {
|
|
795
|
+
this.components.conversationTable = new ConversationTable(
|
|
796
|
+
tableContainer,
|
|
797
|
+
this.dataService,
|
|
798
|
+
this.stateService
|
|
799
|
+
);
|
|
800
|
+
await this.components.conversationTable.initialize();
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.warn('ConversationTable initialization failed:', error);
|
|
803
|
+
// Show fallback content
|
|
804
|
+
tableContainer.innerHTML = `
|
|
805
|
+
<div class="conversation-table-placeholder">
|
|
806
|
+
<p>Detailed table view not available</p>
|
|
807
|
+
</div>
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Bind event listeners
|
|
815
|
+
*/
|
|
816
|
+
bindEvents() {
|
|
817
|
+
// Filter controls
|
|
818
|
+
const statusFilter = this.container.querySelector('#status-filter');
|
|
819
|
+
statusFilter.addEventListener('change', (e) => this.updateFilter('status', e.target.value));
|
|
820
|
+
|
|
821
|
+
const timeFilter = this.container.querySelector('#time-filter');
|
|
822
|
+
timeFilter.addEventListener('change', (e) => this.updateFilter('timeRange', e.target.value));
|
|
823
|
+
|
|
824
|
+
const searchInput = this.container.querySelector('#search-filter');
|
|
825
|
+
searchInput.addEventListener('input', (e) => this.updateFilter('search', e.target.value));
|
|
826
|
+
|
|
827
|
+
const clearSearch = this.container.querySelector('#clear-search');
|
|
828
|
+
clearSearch.addEventListener('click', () => this.clearSearch());
|
|
829
|
+
|
|
830
|
+
// Error retry
|
|
831
|
+
const retryBtn = this.container.querySelector('#retry-load');
|
|
832
|
+
if (retryBtn) {
|
|
833
|
+
retryBtn.addEventListener('click', () => this.loadConversationsData());
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Clear filters
|
|
837
|
+
const clearFiltersBtn = this.container.querySelector('#clear-filters');
|
|
838
|
+
if (clearFiltersBtn) {
|
|
839
|
+
clearFiltersBtn.addEventListener('click', () => this.clearAllFilters());
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Test console interaction
|
|
843
|
+
const testConsoleBtn = this.container.querySelector('#test-console-interaction');
|
|
844
|
+
if (testConsoleBtn) {
|
|
845
|
+
testConsoleBtn.addEventListener('click', () => this.testConsoleInteraction());
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Setup infinite scroll for conversations list
|
|
851
|
+
*/
|
|
852
|
+
setupInfiniteScroll() {
|
|
853
|
+
const conversationsContainer = this.container.querySelector('#conversations-list');
|
|
854
|
+
if (!conversationsContainer) return;
|
|
855
|
+
|
|
856
|
+
conversationsContainer.addEventListener('scroll', () => {
|
|
857
|
+
const { scrollTop, scrollHeight, clientHeight } = conversationsContainer;
|
|
858
|
+
const threshold = 100; // Load more when 100px from bottom
|
|
859
|
+
|
|
860
|
+
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
|
861
|
+
this.loadMoreConversations();
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Update loading indicator
|
|
868
|
+
* @param {boolean} isLoading - Whether to show loading indicator
|
|
869
|
+
*/
|
|
870
|
+
updateLoadingIndicator(isLoading) {
|
|
871
|
+
const loadingIndicator = this.container.querySelector('#load-more-indicator');
|
|
872
|
+
if (loadingIndicator) {
|
|
873
|
+
loadingIndicator.style.display = isLoading ? 'flex' : 'none';
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Load initial conversations data using paginated API
|
|
879
|
+
*/
|
|
880
|
+
async loadConversationsData() {
|
|
881
|
+
try {
|
|
882
|
+
|
|
883
|
+
// Reset pagination state
|
|
884
|
+
this.pagination = {
|
|
885
|
+
currentPage: 0,
|
|
886
|
+
limit: 10,
|
|
887
|
+
hasMore: true,
|
|
888
|
+
isLoading: false
|
|
889
|
+
};
|
|
890
|
+
this.loadedConversations = [];
|
|
891
|
+
this.loadedMessages.clear(); // Clear message cache too
|
|
892
|
+
|
|
893
|
+
// Clear the list container
|
|
894
|
+
const listContainer = this.container.querySelector('#conversations-list');
|
|
895
|
+
if (listContainer) {
|
|
896
|
+
listContainer.innerHTML = '';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Hide empty state initially
|
|
900
|
+
this.hideEmptyState();
|
|
901
|
+
|
|
902
|
+
// Load first page and states
|
|
903
|
+
await this.loadMoreConversations();
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error('Error loading conversations data:', error);
|
|
908
|
+
this.stateService.setError('Failed to load conversations data');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Load more conversations (pagination)
|
|
914
|
+
*/
|
|
915
|
+
async loadMoreConversations() {
|
|
916
|
+
if (this.pagination.isLoading || !this.pagination.hasMore) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
|
|
922
|
+
this.pagination.isLoading = true;
|
|
923
|
+
this.updateLoadingIndicator(true);
|
|
924
|
+
|
|
925
|
+
const [conversationsData, statesData] = await Promise.all([
|
|
926
|
+
this.dataService.getConversationsPaginated(this.pagination.currentPage, this.pagination.limit),
|
|
927
|
+
this.dataService.getConversationStates()
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
// Update pagination info
|
|
932
|
+
this.pagination.hasMore = conversationsData.pagination.hasMore;
|
|
933
|
+
this.pagination.currentPage = conversationsData.pagination.page + 1;
|
|
934
|
+
this.pagination.totalCount = conversationsData.pagination.totalCount;
|
|
935
|
+
|
|
936
|
+
// Get only NEW conversations for this page
|
|
937
|
+
const newConversations = conversationsData.conversations;
|
|
938
|
+
|
|
939
|
+
// Add new conversations to loaded list
|
|
940
|
+
this.loadedConversations.push(...newConversations);
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
// Extract activeStates from the response structure
|
|
944
|
+
const activeStates = statesData?.activeStates || {};
|
|
945
|
+
|
|
946
|
+
// Update state with correct format
|
|
947
|
+
this.stateService.updateConversations(this.loadedConversations);
|
|
948
|
+
this.stateService.updateConversationStates(activeStates);
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
// For initial load (page 0), replace content. For subsequent loads, append
|
|
952
|
+
const isInitialLoad = conversationsData.pagination.page === 0;
|
|
953
|
+
this.renderConversationsList(
|
|
954
|
+
isInitialLoad ? this.loadedConversations : newConversations,
|
|
955
|
+
activeStates,
|
|
956
|
+
!isInitialLoad
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error('Error loading more conversations:', error);
|
|
961
|
+
this.stateService.setError('Failed to load more conversations');
|
|
962
|
+
} finally {
|
|
963
|
+
this.pagination.isLoading = false;
|
|
964
|
+
this.updateLoadingIndicator(false);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Render conversations list
|
|
970
|
+
* @param {Array} conversations - Conversations data
|
|
971
|
+
* @param {Object} states - Conversation states
|
|
972
|
+
* @param {boolean} append - Whether to append or replace content
|
|
973
|
+
*/
|
|
974
|
+
renderConversationsList(conversations, states, append = false) {
|
|
975
|
+
const listContainer = this.container.querySelector('#conversations-list');
|
|
976
|
+
const filteredConversations = this.filterConversations(conversations, states);
|
|
977
|
+
|
|
978
|
+
// Calculate count based on filters
|
|
979
|
+
let countToShow;
|
|
980
|
+
const hasActiveFilters = this.hasActiveFilters();
|
|
981
|
+
|
|
982
|
+
if (!hasActiveFilters && this.pagination && this.pagination.totalCount) {
|
|
983
|
+
// No filters active, show total count from server
|
|
984
|
+
countToShow = this.pagination.totalCount;
|
|
985
|
+
} else {
|
|
986
|
+
// Filters active, count filtered loaded conversations
|
|
987
|
+
const conversationsToCount = this.loadedConversations && this.loadedConversations.length > 0
|
|
988
|
+
? this.loadedConversations
|
|
989
|
+
: conversations;
|
|
990
|
+
const allFilteredConversations = this.filterConversations(conversationsToCount, states);
|
|
991
|
+
countToShow = allFilteredConversations.length;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
this.updateResultsCount(countToShow, hasActiveFilters);
|
|
995
|
+
this.updateClearFiltersButton();
|
|
996
|
+
|
|
997
|
+
if (filteredConversations.length === 0 && !append) {
|
|
998
|
+
this.showEmptyState();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
this.hideEmptyState();
|
|
1003
|
+
|
|
1004
|
+
const conversationHTML = filteredConversations.map(conv => {
|
|
1005
|
+
const state = states[conv.id] || 'unknown';
|
|
1006
|
+
const stateClass = this.getStateClass(state);
|
|
1007
|
+
|
|
1008
|
+
return `
|
|
1009
|
+
<div class="sidebar-conversation-item" data-id="${conv.id}">
|
|
1010
|
+
<div class="sidebar-conversation-header">
|
|
1011
|
+
<div class="sidebar-conversation-title">
|
|
1012
|
+
<span class="status-dot ${stateClass}"></span>
|
|
1013
|
+
<h4 class="sidebar-conversation-name">${conv.title || `Chat ${conv.id.slice(-8)}`}</h4>
|
|
1014
|
+
</div>
|
|
1015
|
+
<span class="sidebar-conversation-badge ${stateClass}">${this.getStateLabel(state)}</span>
|
|
1016
|
+
</div>
|
|
1017
|
+
|
|
1018
|
+
<div class="sidebar-conversation-meta">
|
|
1019
|
+
<span class="sidebar-meta-item">
|
|
1020
|
+
<span class="sidebar-meta-icon">📁</span>
|
|
1021
|
+
${this.truncateText(conv.project || 'Unknown', 12)}
|
|
1022
|
+
</span>
|
|
1023
|
+
</div>
|
|
1024
|
+
|
|
1025
|
+
<div class="sidebar-conversation-preview">
|
|
1026
|
+
<p class="sidebar-preview-text">${this.getSimpleConversationPreview(conv)}</p>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
`;
|
|
1030
|
+
}).join('');
|
|
1031
|
+
|
|
1032
|
+
if (append) {
|
|
1033
|
+
listContainer.insertAdjacentHTML('beforeend', conversationHTML);
|
|
1034
|
+
} else {
|
|
1035
|
+
listContainer.innerHTML = conversationHTML;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Bind card actions
|
|
1039
|
+
this.bindListActions();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Bind list action events
|
|
1044
|
+
*/
|
|
1045
|
+
bindListActions() {
|
|
1046
|
+
// Export conversation button
|
|
1047
|
+
const exportBtn = this.container.querySelector('#export-conversation');
|
|
1048
|
+
if (exportBtn) {
|
|
1049
|
+
exportBtn.addEventListener('click', () => {
|
|
1050
|
+
if (this.selectedConversationId) {
|
|
1051
|
+
this.exportSingleConversation(this.selectedConversationId);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Click on sidebar conversation item to select and view
|
|
1057
|
+
const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
|
|
1058
|
+
conversationItems.forEach(item => {
|
|
1059
|
+
item.addEventListener('click', () => {
|
|
1060
|
+
const conversationId = item.dataset.id;
|
|
1061
|
+
this.selectConversation(conversationId);
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Select and display a conversation
|
|
1068
|
+
* @param {string} conversationId - Conversation ID
|
|
1069
|
+
*/
|
|
1070
|
+
async selectConversation(conversationId) {
|
|
1071
|
+
// Update selected conversation state
|
|
1072
|
+
this.selectedConversationId = conversationId;
|
|
1073
|
+
|
|
1074
|
+
// Update UI to show selection
|
|
1075
|
+
this.updateSelectedConversation();
|
|
1076
|
+
|
|
1077
|
+
// Load and display conversation messages
|
|
1078
|
+
await this.loadConversationMessages(conversationId);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Update selected conversation in sidebar
|
|
1083
|
+
*/
|
|
1084
|
+
updateSelectedConversation() {
|
|
1085
|
+
// Remove previous selection
|
|
1086
|
+
const previousSelected = this.container.querySelector('.sidebar-conversation-item.selected');
|
|
1087
|
+
if (previousSelected) {
|
|
1088
|
+
previousSelected.classList.remove('selected');
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Add selection to current item
|
|
1092
|
+
const currentItem = this.container.querySelector(`[data-id="${this.selectedConversationId}"]`);
|
|
1093
|
+
if (currentItem) {
|
|
1094
|
+
currentItem.classList.add('selected');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Update header with conversation info
|
|
1098
|
+
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
1099
|
+
const conversation = conversations.find(conv => conv.id === this.selectedConversationId);
|
|
1100
|
+
|
|
1101
|
+
if (conversation) {
|
|
1102
|
+
const titleElement = this.container.querySelector('#selected-conversation-title');
|
|
1103
|
+
const metaElement = this.container.querySelector('#selected-conversation-meta');
|
|
1104
|
+
|
|
1105
|
+
if (titleElement) {
|
|
1106
|
+
titleElement.textContent = conversation.title || `Chat ${conversation.id.slice(-8)}`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (metaElement) {
|
|
1110
|
+
const messageCount = conversation.messageCount || 0;
|
|
1111
|
+
const lastActivity = this.formatRelativeTime(new Date(conversation.lastModified));
|
|
1112
|
+
metaElement.innerHTML = `
|
|
1113
|
+
<span class="meta-item">
|
|
1114
|
+
<span class="meta-icon">📁</span>
|
|
1115
|
+
${conversation.project || 'Unknown Project'}
|
|
1116
|
+
</span>
|
|
1117
|
+
<span class="meta-item">
|
|
1118
|
+
<span class="meta-icon">💬</span>
|
|
1119
|
+
${messageCount} message${messageCount !== 1 ? 's' : ''}
|
|
1120
|
+
</span>
|
|
1121
|
+
<span class="meta-item">
|
|
1122
|
+
<span class="meta-icon">🕒</span>
|
|
1123
|
+
${lastActivity}
|
|
1124
|
+
</span>
|
|
1125
|
+
`;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Show and update the state banner
|
|
1130
|
+
this.showStateBanner(this.selectedConversationId);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Load and display conversation messages (with caching)
|
|
1135
|
+
* @param {string} conversationId - Conversation ID
|
|
1136
|
+
*/
|
|
1137
|
+
async loadConversationMessages(conversationId) {
|
|
1138
|
+
// Reset pagination for new conversation
|
|
1139
|
+
this.messagesPagination = {
|
|
1140
|
+
currentPage: 0,
|
|
1141
|
+
limit: 10,
|
|
1142
|
+
hasMore: true,
|
|
1143
|
+
isLoading: false,
|
|
1144
|
+
conversationId: conversationId
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
// Clear cached messages for this conversation
|
|
1148
|
+
this.loadedMessages.delete(conversationId);
|
|
1149
|
+
|
|
1150
|
+
// Load first page of messages
|
|
1151
|
+
await this.loadMoreMessages(conversationId, true);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Show and update conversation state banner
|
|
1156
|
+
* @param {string} conversationId - Conversation ID
|
|
1157
|
+
*/
|
|
1158
|
+
showStateBanner(conversationId) {
|
|
1159
|
+
const banner = this.container.querySelector('#conversation-state-banner');
|
|
1160
|
+
if (!banner) return;
|
|
1161
|
+
|
|
1162
|
+
// Show the banner
|
|
1163
|
+
banner.style.display = 'flex';
|
|
1164
|
+
|
|
1165
|
+
// Get current state from WebSocket or cache
|
|
1166
|
+
const conversationStates = this.stateService.getStateProperty('conversationStates') || {};
|
|
1167
|
+
const currentState = conversationStates[conversationId] || 'unknown';
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
// If we don't have the state yet, try to fetch it after a short delay
|
|
1171
|
+
if (currentState === 'unknown') {
|
|
1172
|
+
setTimeout(() => {
|
|
1173
|
+
this.fetchConversationState(conversationId);
|
|
1174
|
+
}, 100);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Update banner with current state
|
|
1178
|
+
this.updateStateBanner(conversationId, currentState);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Update conversation state banner
|
|
1183
|
+
* @param {string} conversationId - Conversation ID
|
|
1184
|
+
* @param {string} state - Current conversation state
|
|
1185
|
+
*/
|
|
1186
|
+
updateStateBanner(conversationId, state) {
|
|
1187
|
+
const banner = this.container.querySelector('#conversation-state-banner');
|
|
1188
|
+
const stateDot = this.container.querySelector('#state-dot');
|
|
1189
|
+
const stateText = this.container.querySelector('#state-text');
|
|
1190
|
+
const stateTimestamp = this.container.querySelector('#state-timestamp');
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
if (!banner || !stateDot || !stateText || !stateTimestamp) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Map states to user-friendly messages with enhanced descriptions
|
|
1198
|
+
const stateMessages = {
|
|
1199
|
+
'Claude Code working...': {
|
|
1200
|
+
text: '🤖 Claude is thinking and working...',
|
|
1201
|
+
description: 'Claude is processing your request',
|
|
1202
|
+
class: 'status-working',
|
|
1203
|
+
icon: '🧠'
|
|
1204
|
+
},
|
|
1205
|
+
'Awaiting tool response...': {
|
|
1206
|
+
text: '⚡ Waiting for tool execution...',
|
|
1207
|
+
description: 'Claude is waiting for tool results',
|
|
1208
|
+
class: 'status-tool-pending',
|
|
1209
|
+
icon: '🔧'
|
|
1210
|
+
},
|
|
1211
|
+
'Executing tools...': {
|
|
1212
|
+
text: '🔧 Executing tools...',
|
|
1213
|
+
description: 'Claude is running system tools',
|
|
1214
|
+
class: 'status-tool-executing',
|
|
1215
|
+
icon: '⚡'
|
|
1216
|
+
},
|
|
1217
|
+
'Analyzing results...': {
|
|
1218
|
+
text: '📊 Analyzing tool results...',
|
|
1219
|
+
description: 'Claude is processing tool outputs',
|
|
1220
|
+
class: 'status-analyzing',
|
|
1221
|
+
icon: '🔍'
|
|
1222
|
+
},
|
|
1223
|
+
'Analyzing code...': {
|
|
1224
|
+
text: '🔍 Analyzing code...',
|
|
1225
|
+
description: 'Claude is examining code or files',
|
|
1226
|
+
class: 'status-analyzing',
|
|
1227
|
+
icon: '📝'
|
|
1228
|
+
},
|
|
1229
|
+
'Fetching data...': {
|
|
1230
|
+
text: '🌐 Fetching data...',
|
|
1231
|
+
description: 'Claude is retrieving web content or external data',
|
|
1232
|
+
class: 'status-fetching',
|
|
1233
|
+
icon: '📶'
|
|
1234
|
+
},
|
|
1235
|
+
'Task completed': {
|
|
1236
|
+
text: '✅ Task completed',
|
|
1237
|
+
description: 'Claude has finished the requested task',
|
|
1238
|
+
class: 'status-completed',
|
|
1239
|
+
icon: '✨'
|
|
1240
|
+
},
|
|
1241
|
+
'Processing request...': {
|
|
1242
|
+
text: '⚙️ Processing request...',
|
|
1243
|
+
description: 'Claude is working on a complex request',
|
|
1244
|
+
class: 'status-processing',
|
|
1245
|
+
icon: '🔄'
|
|
1246
|
+
},
|
|
1247
|
+
'Encountered issue': {
|
|
1248
|
+
text: '⚠️ Encountered issue',
|
|
1249
|
+
description: 'Claude found an error or problem',
|
|
1250
|
+
class: 'status-error',
|
|
1251
|
+
icon: '🚟'
|
|
1252
|
+
},
|
|
1253
|
+
'Awaiting user input...': {
|
|
1254
|
+
text: '💬 Awaiting your input',
|
|
1255
|
+
description: 'Claude needs your response to continue',
|
|
1256
|
+
class: 'status-waiting',
|
|
1257
|
+
icon: '💭'
|
|
1258
|
+
},
|
|
1259
|
+
'Waiting for your response': {
|
|
1260
|
+
text: '💬 Waiting for your response',
|
|
1261
|
+
description: 'Claude is ready for your next message',
|
|
1262
|
+
class: 'status-waiting-response',
|
|
1263
|
+
icon: '📝'
|
|
1264
|
+
},
|
|
1265
|
+
'Awaiting response...': {
|
|
1266
|
+
text: '⏳ Awaiting Claude response',
|
|
1267
|
+
description: 'Waiting for Claude to respond',
|
|
1268
|
+
class: 'status-waiting',
|
|
1269
|
+
icon: '🤔'
|
|
1270
|
+
},
|
|
1271
|
+
'Recently active': {
|
|
1272
|
+
text: '🟢 Recently active',
|
|
1273
|
+
description: 'Conversation was active recently',
|
|
1274
|
+
class: 'status-active',
|
|
1275
|
+
icon: '✨'
|
|
1276
|
+
},
|
|
1277
|
+
'Idle': {
|
|
1278
|
+
text: '😴 Conversation idle',
|
|
1279
|
+
description: 'No recent activity',
|
|
1280
|
+
class: 'status-idle',
|
|
1281
|
+
icon: '💤'
|
|
1282
|
+
},
|
|
1283
|
+
'Inactive': {
|
|
1284
|
+
text: '⚪ Inactive',
|
|
1285
|
+
description: 'Conversation has been inactive',
|
|
1286
|
+
class: 'status-idle',
|
|
1287
|
+
icon: '⏸️'
|
|
1288
|
+
},
|
|
1289
|
+
'Old': {
|
|
1290
|
+
text: '📚 Archived conversation',
|
|
1291
|
+
description: 'No recent activity in this conversation',
|
|
1292
|
+
class: 'status-idle',
|
|
1293
|
+
icon: '📁'
|
|
1294
|
+
},
|
|
1295
|
+
'unknown': {
|
|
1296
|
+
text: '🔄 Loading conversation state...',
|
|
1297
|
+
description: 'Determining conversation status',
|
|
1298
|
+
class: 'status-loading',
|
|
1299
|
+
icon: '⏳'
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const stateInfo = stateMessages[state] || stateMessages['unknown'];
|
|
1304
|
+
|
|
1305
|
+
// Update dot class with enhanced styling
|
|
1306
|
+
stateDot.className = `state-dot ${stateInfo.class}`;
|
|
1307
|
+
|
|
1308
|
+
// Update text with icon and description
|
|
1309
|
+
stateText.innerHTML = `
|
|
1310
|
+
<span class="state-text-main">${stateInfo.text}</span>
|
|
1311
|
+
<span class="state-text-description">${stateInfo.description}</span>
|
|
1312
|
+
`;
|
|
1313
|
+
|
|
1314
|
+
// Add tooltip for additional context
|
|
1315
|
+
stateText.title = stateInfo.description;
|
|
1316
|
+
|
|
1317
|
+
// Update timestamp with more context
|
|
1318
|
+
const now = new Date();
|
|
1319
|
+
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
1320
|
+
stateTimestamp.innerHTML = `
|
|
1321
|
+
<span class="timestamp-label">Last updated:</span>
|
|
1322
|
+
<span class="timestamp-value">${timeString}</span>
|
|
1323
|
+
`;
|
|
1324
|
+
|
|
1325
|
+
// Add pulsing animation for active states
|
|
1326
|
+
if (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing')) {
|
|
1327
|
+
stateDot.classList.add('pulse-animation');
|
|
1328
|
+
setTimeout(() => stateDot.classList.remove('pulse-animation'), 3000);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Fetch conversation state from API
|
|
1335
|
+
* @param {string} conversationId - Conversation ID
|
|
1336
|
+
*/
|
|
1337
|
+
async fetchConversationState(conversationId) {
|
|
1338
|
+
try {
|
|
1339
|
+
const stateData = await this.dataService.getConversationStates();
|
|
1340
|
+
|
|
1341
|
+
if (stateData && stateData.activeStates && stateData.activeStates[conversationId]) {
|
|
1342
|
+
const state = stateData.activeStates[conversationId];
|
|
1343
|
+
|
|
1344
|
+
// Update the StateService with the new data
|
|
1345
|
+
this.stateService.updateConversationStates(stateData.activeStates);
|
|
1346
|
+
|
|
1347
|
+
// Update the banner with the real state
|
|
1348
|
+
this.updateStateBanner(conversationId, state);
|
|
1349
|
+
} else {
|
|
1350
|
+
// Keep showing unknown for now
|
|
1351
|
+
}
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
console.error('Error fetching conversation state:', error);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Hide conversation state banner
|
|
1359
|
+
*/
|
|
1360
|
+
hideStateBanner() {
|
|
1361
|
+
const banner = this.container.querySelector('#conversation-state-banner');
|
|
1362
|
+
if (banner) {
|
|
1363
|
+
banner.style.display = 'none';
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Auto-scroll to bottom of messages
|
|
1369
|
+
*/
|
|
1370
|
+
scrollToBottom() {
|
|
1371
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
1372
|
+
if (messagesContent) {
|
|
1373
|
+
messagesContent.scrollTop = messagesContent.scrollHeight;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Show notification for new message
|
|
1379
|
+
* @param {Object} message - New message object
|
|
1380
|
+
* @param {Object} metadata - Message metadata
|
|
1381
|
+
*/
|
|
1382
|
+
showNewMessageNotification(message, metadata) {
|
|
1383
|
+
// Update banner if it's showing to reflect new activity
|
|
1384
|
+
if (this.selectedConversationId) {
|
|
1385
|
+
const banner = this.container.querySelector('#conversation-state-banner');
|
|
1386
|
+
if (banner && banner.style.display !== 'none') {
|
|
1387
|
+
// Temporarily highlight the banner to show activity
|
|
1388
|
+
banner.style.backgroundColor = 'rgba(213, 116, 85, 0.1)';
|
|
1389
|
+
setTimeout(() => {
|
|
1390
|
+
banner.style.backgroundColor = '';
|
|
1391
|
+
}, 1000);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Could add visual indicator for new message (pulse, notification badge, etc.)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Load more messages (for infinite scroll)
|
|
1400
|
+
* @param {string} conversationId - Conversation ID
|
|
1401
|
+
* @param {boolean} isInitialLoad - Whether this is the initial load
|
|
1402
|
+
*/
|
|
1403
|
+
async loadMoreMessages(conversationId, isInitialLoad = false) {
|
|
1404
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
1405
|
+
if (!messagesContent) return;
|
|
1406
|
+
|
|
1407
|
+
// Prevent concurrent loading
|
|
1408
|
+
if (this.messagesPagination.isLoading || !this.messagesPagination.hasMore) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Ensure we're loading for the correct conversation
|
|
1413
|
+
if (this.messagesPagination.conversationId !== conversationId) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
try {
|
|
1418
|
+
this.messagesPagination.isLoading = true;
|
|
1419
|
+
|
|
1420
|
+
if (isInitialLoad) {
|
|
1421
|
+
// Show loading state for initial load
|
|
1422
|
+
messagesContent.innerHTML = `
|
|
1423
|
+
<div class="messages-loading">
|
|
1424
|
+
<div class="loading-spinner"></div>
|
|
1425
|
+
<span>Loading messages...</span>
|
|
1426
|
+
</div>
|
|
1427
|
+
`;
|
|
1428
|
+
} else {
|
|
1429
|
+
// Show loading indicator at top for infinite scroll
|
|
1430
|
+
this.showMessagesLoadingIndicator(true);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Fetch paginated messages from the server
|
|
1434
|
+
const messagesData = await this.dataService.cachedFetch(
|
|
1435
|
+
`/api/conversations/${conversationId}/messages?page=${this.messagesPagination.currentPage}&limit=${this.messagesPagination.limit}`
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
if (messagesData && messagesData.messages) {
|
|
1439
|
+
// Update pagination state - handle both paginated and non-paginated responses
|
|
1440
|
+
if (messagesData.pagination) {
|
|
1441
|
+
// Paginated response
|
|
1442
|
+
this.messagesPagination.hasMore = messagesData.pagination.hasMore;
|
|
1443
|
+
this.messagesPagination.currentPage = messagesData.pagination.page + 1;
|
|
1444
|
+
} else {
|
|
1445
|
+
// Non-paginated response (fallback) - treat as complete data
|
|
1446
|
+
this.messagesPagination.hasMore = false;
|
|
1447
|
+
this.messagesPagination.currentPage = 1;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Get existing messages or initialize
|
|
1451
|
+
let existingMessages = this.loadedMessages.get(conversationId) || [];
|
|
1452
|
+
|
|
1453
|
+
if (isInitialLoad) {
|
|
1454
|
+
// For initial load, replace all messages
|
|
1455
|
+
existingMessages = messagesData.messages;
|
|
1456
|
+
} else {
|
|
1457
|
+
// For infinite scroll, prepend older messages (they come in chronological order)
|
|
1458
|
+
existingMessages = [...messagesData.messages, ...existingMessages];
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Cache the combined messages
|
|
1462
|
+
this.loadedMessages.set(conversationId, existingMessages);
|
|
1463
|
+
|
|
1464
|
+
// Render messages
|
|
1465
|
+
this.renderCachedMessages(existingMessages, !isInitialLoad);
|
|
1466
|
+
|
|
1467
|
+
// Setup scroll listener for infinite scroll (only on initial load)
|
|
1468
|
+
if (isInitialLoad) {
|
|
1469
|
+
this.setupMessagesScrollListener(conversationId);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
} else if (isInitialLoad) {
|
|
1474
|
+
messagesContent.innerHTML = `
|
|
1475
|
+
<div class="no-messages-found">
|
|
1476
|
+
<div class="no-messages-icon">💭</div>
|
|
1477
|
+
<h4>No messages found</h4>
|
|
1478
|
+
<p>This conversation has no messages or they could not be loaded.</p>
|
|
1479
|
+
</div>
|
|
1480
|
+
`;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
console.error('Error loading messages:', error);
|
|
1485
|
+
|
|
1486
|
+
if (isInitialLoad) {
|
|
1487
|
+
messagesContent.innerHTML = `
|
|
1488
|
+
<div class="messages-error">
|
|
1489
|
+
<span class="error-icon">⚠️</span>
|
|
1490
|
+
<span>Failed to load messages</span>
|
|
1491
|
+
<button class="retry-messages" data-conversation-id="${conversationId}">Retry</button>
|
|
1492
|
+
</div>
|
|
1493
|
+
`;
|
|
1494
|
+
|
|
1495
|
+
// Bind retry button event
|
|
1496
|
+
const retryBtn = messagesContent.querySelector('.retry-messages');
|
|
1497
|
+
if (retryBtn) {
|
|
1498
|
+
retryBtn.addEventListener('click', () => {
|
|
1499
|
+
this.loadConversationMessages(conversationId);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
} finally {
|
|
1504
|
+
this.messagesPagination.isLoading = false;
|
|
1505
|
+
if (!isInitialLoad) {
|
|
1506
|
+
this.showMessagesLoadingIndicator(false);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Render cached messages
|
|
1513
|
+
* @param {Array} messages - Array of messages
|
|
1514
|
+
* @param {boolean} prepend - Whether to prepend messages (for infinite scroll)
|
|
1515
|
+
*/
|
|
1516
|
+
renderCachedMessages(messages, prepend = false) {
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
1520
|
+
if (!messagesContent) {
|
|
1521
|
+
console.warn(`⚠️ messages-content element not found!`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Store messages globally for tool result lookup
|
|
1526
|
+
if (typeof window !== 'undefined') {
|
|
1527
|
+
window.currentMessages = messages;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const messageHTML = `
|
|
1531
|
+
<div class="messages-loading-indicator" style="display: none;">
|
|
1532
|
+
<div class="loading-spinner small"></div>
|
|
1533
|
+
<span>Loading older messages...</span>
|
|
1534
|
+
</div>
|
|
1535
|
+
<div class="messages-list">
|
|
1536
|
+
${messages.map(msg => this.renderMessage(msg)).join('')}
|
|
1537
|
+
</div>
|
|
1538
|
+
`;
|
|
1539
|
+
|
|
1540
|
+
if (prepend) {
|
|
1541
|
+
// For infinite scroll, we need to maintain scroll position
|
|
1542
|
+
const oldScrollHeight = messagesContent.scrollHeight;
|
|
1543
|
+
|
|
1544
|
+
// Update content
|
|
1545
|
+
messagesContent.innerHTML = messageHTML;
|
|
1546
|
+
|
|
1547
|
+
// Restore scroll position relative to the bottom
|
|
1548
|
+
const newScrollHeight = messagesContent.scrollHeight;
|
|
1549
|
+
const scrollDifference = newScrollHeight - oldScrollHeight;
|
|
1550
|
+
messagesContent.scrollTop += scrollDifference;
|
|
1551
|
+
} else {
|
|
1552
|
+
// Initial load - just replace content and scroll to bottom
|
|
1553
|
+
messagesContent.innerHTML = messageHTML;
|
|
1554
|
+
|
|
1555
|
+
// Scroll to bottom for new conversation load
|
|
1556
|
+
setTimeout(() => {
|
|
1557
|
+
messagesContent.scrollTop = messagesContent.scrollHeight;
|
|
1558
|
+
}, 100);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Bind tool display events
|
|
1562
|
+
this.toolDisplay.bindEvents(messagesContent);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Show/hide messages loading indicator
|
|
1567
|
+
* @param {boolean} show - Whether to show the indicator
|
|
1568
|
+
*/
|
|
1569
|
+
showMessagesLoadingIndicator(show) {
|
|
1570
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
1571
|
+
if (!messagesContent) return;
|
|
1572
|
+
|
|
1573
|
+
const indicator = messagesContent.querySelector('.messages-loading-indicator');
|
|
1574
|
+
if (indicator) {
|
|
1575
|
+
indicator.style.display = show ? 'flex' : 'none';
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Setup scroll listener for infinite scroll in messages
|
|
1581
|
+
* @param {string} conversationId - Current conversation ID
|
|
1582
|
+
*/
|
|
1583
|
+
setupMessagesScrollListener(conversationId) {
|
|
1584
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
1585
|
+
if (!messagesContent) return;
|
|
1586
|
+
|
|
1587
|
+
// Remove existing listener if any
|
|
1588
|
+
if (this.messagesScrollListener) {
|
|
1589
|
+
messagesContent.removeEventListener('scroll', this.messagesScrollListener);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Create new listener
|
|
1593
|
+
this.messagesScrollListener = () => {
|
|
1594
|
+
// Check if we've scrolled near the top (for loading older messages)
|
|
1595
|
+
const scrollTop = messagesContent.scrollTop;
|
|
1596
|
+
const threshold = 100; // pixels from top
|
|
1597
|
+
|
|
1598
|
+
if (scrollTop <= threshold && this.messagesPagination.hasMore && !this.messagesPagination.isLoading) {
|
|
1599
|
+
this.loadMoreMessages(conversationId, false);
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
// Add listener
|
|
1604
|
+
messagesContent.addEventListener('scroll', this.messagesScrollListener);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Render a single message with terminal-style formatting
|
|
1609
|
+
* @param {Object} message - Message object
|
|
1610
|
+
* @returns {string} HTML string
|
|
1611
|
+
*/
|
|
1612
|
+
renderMessage(message) {
|
|
1613
|
+
const timestamp = this.formatRelativeTime(new Date(message.timestamp));
|
|
1614
|
+
const fullTimestamp = new Date(message.timestamp).toLocaleString();
|
|
1615
|
+
// Compact summaries should be displayed as assistant messages even if marked as 'user'
|
|
1616
|
+
const isUser = message.role === 'user' && !message.isCompactSummary;
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
// Detect if message contains tools
|
|
1620
|
+
const hasTools = Array.isArray(message.content) &&
|
|
1621
|
+
message.content.some(block => block.type === 'tool_use');
|
|
1622
|
+
const toolCount = hasTools ?
|
|
1623
|
+
message.content.filter(block => block.type === 'tool_use').length : 0;
|
|
1624
|
+
|
|
1625
|
+
// Terminal-style prompt
|
|
1626
|
+
const prompt = isUser ? '>' : '#';
|
|
1627
|
+
const roleLabel = isUser ? 'user' : 'claude';
|
|
1628
|
+
|
|
1629
|
+
// Get message ID (short version for display)
|
|
1630
|
+
const messageId = message.id ? message.id.slice(-8) : 'unknown';
|
|
1631
|
+
|
|
1632
|
+
return `
|
|
1633
|
+
<div class="terminal-message ${isUser ? 'user' : 'assistant'}" data-message-id="${message.id || ''}">
|
|
1634
|
+
<div class="message-container">
|
|
1635
|
+
<div class="message-prompt">
|
|
1636
|
+
<span class="prompt-char">${prompt}</span>
|
|
1637
|
+
<div class="message-metadata">
|
|
1638
|
+
<span class="timestamp" title="${fullTimestamp}">${timestamp}</span>
|
|
1639
|
+
<span class="role-label">${roleLabel}</span>
|
|
1640
|
+
<span class="message-id" title="Message ID: ${message.id || 'unknown'}">[${messageId}]</span>
|
|
1641
|
+
${message.usage ? `
|
|
1642
|
+
<span class="tokens">
|
|
1643
|
+
${message.usage.input_tokens > 0 ? `i:${message.usage.input_tokens}` : ''}
|
|
1644
|
+
${message.usage.output_tokens > 0 ? `o:${message.usage.output_tokens}` : ''}
|
|
1645
|
+
${message.usage.cache_read_input_tokens > 0 ? `c:${message.usage.cache_read_input_tokens}` : ''}
|
|
1646
|
+
</span>
|
|
1647
|
+
` : ''}
|
|
1648
|
+
${hasTools ? `<span class="tool-count">[${toolCount}t]</span>` : ''}
|
|
1649
|
+
${message.model ? `<span class="model">[${message.model.replace('claude-', '').replace('-20250514', '')}]</span>` : ''}
|
|
1650
|
+
</div>
|
|
1651
|
+
</div>
|
|
1652
|
+
<div class="message-body">
|
|
1653
|
+
${this.formatMessageContent(message.content, message)}
|
|
1654
|
+
</div>
|
|
1655
|
+
</div>
|
|
1656
|
+
</div>
|
|
1657
|
+
`;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Format message content with support for text and tool calls
|
|
1663
|
+
* @param {string|Array} content - Message content
|
|
1664
|
+
* @returns {string} Formatted HTML
|
|
1665
|
+
*/
|
|
1666
|
+
formatMessageContent(content, message = null) {
|
|
1667
|
+
let result = '';
|
|
1668
|
+
|
|
1669
|
+
// Handle different content formats
|
|
1670
|
+
if (Array.isArray(content)) {
|
|
1671
|
+
// Assistant messages with content blocks
|
|
1672
|
+
content.forEach((block, index) => {
|
|
1673
|
+
if (block.type === 'text') {
|
|
1674
|
+
result += this.formatTextContent(block.text);
|
|
1675
|
+
} else if (block.type === 'tool_use') {
|
|
1676
|
+
// Log only tool rendering for debugging
|
|
1677
|
+
console.log('🔧 WebSocket: Rendering tool', { name: block.name, hasResults: !!message?.toolResults });
|
|
1678
|
+
result += this.toolDisplay.renderToolUse(block, message?.toolResults);
|
|
1679
|
+
} else if (block.type === 'tool_result') {
|
|
1680
|
+
result += this.toolDisplay.renderToolResult(block);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
} else if (typeof content === 'string' && content.trim() !== '') {
|
|
1684
|
+
// User messages with plain text - check for special patterns
|
|
1685
|
+
if (content.includes('Tool Result') && content.length > 1000) {
|
|
1686
|
+
// This is likely a large tool result that should be handled specially
|
|
1687
|
+
result += this.formatLargeToolResult(content);
|
|
1688
|
+
} else {
|
|
1689
|
+
// Check if this is a confirmation response "[ok]" or similar
|
|
1690
|
+
const enhancedContent = this.enhanceConfirmationMessage(content, message);
|
|
1691
|
+
result = this.formatTextContent(enhancedContent);
|
|
1692
|
+
}
|
|
1693
|
+
} else if (content && typeof content === 'object') {
|
|
1694
|
+
// Handle edge cases where content might be an object
|
|
1695
|
+
result = this.formatTextContent(JSON.stringify(content, null, 2));
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
return result || '<em class="empty-content">No displayable content available</em>';
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Format regular text content with enhanced Markdown support
|
|
1703
|
+
* @param {string} text - Text content
|
|
1704
|
+
* @returns {string} Formatted HTML
|
|
1705
|
+
*/
|
|
1706
|
+
/**
|
|
1707
|
+
* Apply markdown formatting to HTML-escaped text
|
|
1708
|
+
* @param {string} escapedText - HTML-escaped text to format
|
|
1709
|
+
* @returns {string} Formatted text with markdown styling
|
|
1710
|
+
*/
|
|
1711
|
+
applyMarkdownFormatting(escapedText) {
|
|
1712
|
+
let formattedText = escapedText;
|
|
1713
|
+
|
|
1714
|
+
// 1. Code blocks (must be first to avoid conflicts)
|
|
1715
|
+
formattedText = formattedText
|
|
1716
|
+
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre class="code-block" data-language="$1"><code>$2</code></pre>');
|
|
1717
|
+
|
|
1718
|
+
// 2. Headers (h1-h6)
|
|
1719
|
+
formattedText = formattedText
|
|
1720
|
+
.replace(/^### (.*$)/gm, '<h3 class="markdown-h3">$1</h3>')
|
|
1721
|
+
.replace(/^## (.*$)/gm, '<h2 class="markdown-h2">$1</h2>')
|
|
1722
|
+
.replace(/^# (.*$)/gm, '<h1 class="markdown-h1">$1</h1>')
|
|
1723
|
+
.replace(/^#### (.*$)/gm, '<h4 class="markdown-h4">$1</h4>')
|
|
1724
|
+
.replace(/^##### (.*$)/gm, '<h5 class="markdown-h5">$1</h5>')
|
|
1725
|
+
.replace(/^###### (.*$)/gm, '<h6 class="markdown-h6">$1</h6>');
|
|
1726
|
+
|
|
1727
|
+
// 3. Bold and italic text
|
|
1728
|
+
formattedText = formattedText
|
|
1729
|
+
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
|
|
1730
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong class="markdown-bold">$1</strong>')
|
|
1731
|
+
.replace(/\*(.*?)\*/g, '<em class="markdown-italic">$1</em>')
|
|
1732
|
+
.replace(/\_\_\_(.*?)\_\_\_/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
|
|
1733
|
+
.replace(/\_\_(.*?)\_\_/g, '<strong class="markdown-bold">$1</strong>')
|
|
1734
|
+
.replace(/\_(.*?)\_/g, '<em class="markdown-italic">$1</em>');
|
|
1735
|
+
|
|
1736
|
+
// 4. Strikethrough
|
|
1737
|
+
formattedText = formattedText
|
|
1738
|
+
.replace(/~~(.*?)~~/g, '<del class="markdown-strikethrough">$1</del>');
|
|
1739
|
+
|
|
1740
|
+
// 5. Links
|
|
1741
|
+
formattedText = formattedText
|
|
1742
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="markdown-link" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1743
|
+
|
|
1744
|
+
// 6. Inline code (after other formatting to avoid conflicts)
|
|
1745
|
+
formattedText = formattedText
|
|
1746
|
+
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
1747
|
+
|
|
1748
|
+
// 7. Lists (unordered)
|
|
1749
|
+
formattedText = formattedText
|
|
1750
|
+
.replace(/^[\s]*[\*\-\+][\s]+(.*)$/gm, '<li class="markdown-list-item">$1</li>');
|
|
1751
|
+
|
|
1752
|
+
// 8. Lists (ordered)
|
|
1753
|
+
formattedText = formattedText
|
|
1754
|
+
.replace(/^[\s]*\d+\.[\s]+(.*)$/gm, '<li class="markdown-ordered-item">$1</li>');
|
|
1755
|
+
|
|
1756
|
+
// 9. Wrap consecutive list items in ul/ol tags
|
|
1757
|
+
formattedText = formattedText
|
|
1758
|
+
.replace(/(<li class="markdown-list-item">.*<\/li>)/gs, (match) => {
|
|
1759
|
+
return '<ul class="markdown-list">' + match + '</ul>';
|
|
1760
|
+
})
|
|
1761
|
+
.replace(/(<li class="markdown-ordered-item">.*<\/li>)/gs, (match) => {
|
|
1762
|
+
return '<ol class="markdown-ordered-list">' + match + '</ol>';
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// 10. Blockquotes
|
|
1766
|
+
formattedText = formattedText
|
|
1767
|
+
.replace(/^>[\s]*(.*)$/gm, '<blockquote class="markdown-blockquote">$1</blockquote>');
|
|
1768
|
+
|
|
1769
|
+
// 11. Horizontal rules
|
|
1770
|
+
formattedText = formattedText
|
|
1771
|
+
.replace(/^[\s]*---[\s]*$/gm, '<hr class="markdown-hr">')
|
|
1772
|
+
.replace(/^[\s]*\*\*\*[\s]*$/gm, '<hr class="markdown-hr">');
|
|
1773
|
+
|
|
1774
|
+
// 12. Line breaks (last to avoid conflicts)
|
|
1775
|
+
formattedText = formattedText
|
|
1776
|
+
.replace(/\n\n/g, '</p><p class="markdown-paragraph">')
|
|
1777
|
+
.replace(/\n/g, '<br>');
|
|
1778
|
+
|
|
1779
|
+
// 13. Wrap in paragraph if not already wrapped
|
|
1780
|
+
if (!formattedText.includes('<p') && !formattedText.includes('<h') &&
|
|
1781
|
+
!formattedText.includes('<ul') && !formattedText.includes('<ol') &&
|
|
1782
|
+
!formattedText.includes('<blockquote')) {
|
|
1783
|
+
formattedText = '<p class="markdown-paragraph">' + formattedText + '</p>';
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
return formattedText;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
formatTextContent(text) {
|
|
1790
|
+
if (!text || text.trim() === '') return '';
|
|
1791
|
+
|
|
1792
|
+
// Escape HTML to prevent XSS
|
|
1793
|
+
const escapeHtml = (str) => {
|
|
1794
|
+
const div = document.createElement('div');
|
|
1795
|
+
div.textContent = str;
|
|
1796
|
+
return div.innerHTML;
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
// Check if text is too long and needs truncation
|
|
1800
|
+
const lines = text.split('\n');
|
|
1801
|
+
const maxVisibleLines = 20; // Increased from 5 to 20 for better visibility
|
|
1802
|
+
|
|
1803
|
+
if (lines.length > maxVisibleLines) {
|
|
1804
|
+
const visibleLines = lines.slice(0, maxVisibleLines);
|
|
1805
|
+
const hiddenLinesCount = lines.length - maxVisibleLines;
|
|
1806
|
+
const contentId = 'text_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
1807
|
+
|
|
1808
|
+
// Store full content for modal
|
|
1809
|
+
if (typeof window !== 'undefined') {
|
|
1810
|
+
window.storedContent = window.storedContent || {};
|
|
1811
|
+
window.storedContent[contentId] = text;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const previewText = escapeHtml(visibleLines.join('\n'));
|
|
1815
|
+
const showMoreButton = `<button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${hiddenLinesCount} lines</button>`;
|
|
1816
|
+
|
|
1817
|
+
// Apply markdown formatting to preview
|
|
1818
|
+
let formattedPreview = this.applyMarkdownFormatting(previewText);
|
|
1819
|
+
|
|
1820
|
+
return `<div class="text-content-preview">${formattedPreview}<div class="text-expand-section"><span class="continuation">… +${hiddenLinesCount} lines hidden</span> ${showMoreButton}</div></div>`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// For non-truncated content, apply full formatting
|
|
1824
|
+
let formattedText = escapeHtml(text);
|
|
1825
|
+
formattedText = this.applyMarkdownFormatting(formattedText);
|
|
1826
|
+
|
|
1827
|
+
return formattedText;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/**
|
|
1831
|
+
* Format large tool result content safely
|
|
1832
|
+
* @param {string} content - Large tool result content
|
|
1833
|
+
* @returns {string} Safe formatted content
|
|
1834
|
+
*/
|
|
1835
|
+
formatLargeToolResult(content) {
|
|
1836
|
+
// Extract tool result ID if present
|
|
1837
|
+
const toolIdMatch = content.match(/Tool Result\s+([A-Za-z0-9]+)/);
|
|
1838
|
+
const toolId = toolIdMatch ? toolIdMatch[1] : 'unknown';
|
|
1839
|
+
|
|
1840
|
+
const escapeHtml = (str) => {
|
|
1841
|
+
const div = document.createElement('div');
|
|
1842
|
+
div.textContent = str;
|
|
1843
|
+
return div.innerHTML;
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
const preview = content.length > 80
|
|
1847
|
+
? escapeHtml(content.substring(0, 80)) + '...'
|
|
1848
|
+
: escapeHtml(content);
|
|
1849
|
+
|
|
1850
|
+
return `
|
|
1851
|
+
<div class="terminal-tool tool-result large">
|
|
1852
|
+
<span class="tool-prompt">></span>
|
|
1853
|
+
<span class="tool-status">[LARGE]</span>
|
|
1854
|
+
<span class="tool-id">[${toolId}]</span>
|
|
1855
|
+
<span class="tool-output">${content.length}b: ${preview}</span>
|
|
1856
|
+
</div>
|
|
1857
|
+
`;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Enhance confirmation messages like "[ok]" with context information
|
|
1862
|
+
* @param {string} content - Original message content
|
|
1863
|
+
* @param {Object} message - Full message object with metadata
|
|
1864
|
+
* @returns {string} Enhanced message content
|
|
1865
|
+
*/
|
|
1866
|
+
enhanceConfirmationMessage(content, message) {
|
|
1867
|
+
const trimmedContent = content.trim();
|
|
1868
|
+
|
|
1869
|
+
// Detect simple confirmation patterns
|
|
1870
|
+
const confirmationPatterns = [
|
|
1871
|
+
/^\[ok\]$/i,
|
|
1872
|
+
/^ok$/i,
|
|
1873
|
+
/^yes$/i,
|
|
1874
|
+
/^\[yes\]$/i,
|
|
1875
|
+
/^y$/i,
|
|
1876
|
+
/^\[y\]$/i,
|
|
1877
|
+
/^1$/, // Choice selection
|
|
1878
|
+
/^2$/,
|
|
1879
|
+
/^3$/
|
|
1880
|
+
];
|
|
1881
|
+
|
|
1882
|
+
const isConfirmation = confirmationPatterns.some(pattern => pattern.test(trimmedContent));
|
|
1883
|
+
|
|
1884
|
+
if (isConfirmation && message) {
|
|
1885
|
+
// Try to extract context from the message timestamp
|
|
1886
|
+
const messageTime = message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : 'unknown time';
|
|
1887
|
+
|
|
1888
|
+
// Enhanced display for confirmation messages
|
|
1889
|
+
return `${content} <span class="confirmation-context">(User confirmation at ${messageTime})</span>`;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// For other potential confirmation-like messages, check if they seem like choices
|
|
1893
|
+
if (/^[1-9]$/.test(trimmedContent)) {
|
|
1894
|
+
return `${content} <span class="confirmation-context">(Menu selection)</span>`;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Check for common CLI responses
|
|
1898
|
+
if (/^(continue|proceed|accept|confirm|done)$/i.test(trimmedContent)) {
|
|
1899
|
+
return `${content} <span class="confirmation-context">(User command)</span>`;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
return content;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Format relative time
|
|
1907
|
+
* @param {Date} date - Date to format
|
|
1908
|
+
* @returns {string} Relative time string
|
|
1909
|
+
*/
|
|
1910
|
+
formatRelativeTime(date) {
|
|
1911
|
+
const now = new Date();
|
|
1912
|
+
const diffMs = now - date;
|
|
1913
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
1914
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
1915
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
1916
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
1917
|
+
|
|
1918
|
+
if (diffSecs < 60) return 'Just now';
|
|
1919
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
1920
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
1921
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
1922
|
+
return date.toLocaleDateString();
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Update clear filters button visibility
|
|
1927
|
+
*/
|
|
1928
|
+
updateClearFiltersButton() {
|
|
1929
|
+
const clearBtn = this.container.querySelector('#clear-filters');
|
|
1930
|
+
if (!clearBtn) return; // Guard against null when AgentsPage isn't rendered
|
|
1931
|
+
|
|
1932
|
+
const hasActiveFilters = this.filters.status !== 'all' ||
|
|
1933
|
+
this.filters.timeRange !== '7d' ||
|
|
1934
|
+
this.filters.search !== '';
|
|
1935
|
+
clearBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* Handle list action
|
|
1940
|
+
* @param {string} action - Action type
|
|
1941
|
+
* @param {string} conversationId - Conversation ID
|
|
1942
|
+
*/
|
|
1943
|
+
handleListAction(action, conversationId) {
|
|
1944
|
+
switch (action) {
|
|
1945
|
+
case 'view':
|
|
1946
|
+
this.viewConversation(conversationId);
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Filter conversations based on current filters
|
|
1953
|
+
* @param {Array} conversations - All conversations
|
|
1954
|
+
* @param {Object} states - Conversation states
|
|
1955
|
+
* @returns {Array} Filtered conversations
|
|
1956
|
+
*/
|
|
1957
|
+
filterConversations(conversations, states) {
|
|
1958
|
+
let filtered = conversations;
|
|
1959
|
+
|
|
1960
|
+
// Filter by status
|
|
1961
|
+
if (this.filters.status !== 'all') {
|
|
1962
|
+
filtered = filtered.filter(conv => {
|
|
1963
|
+
const state = states[conv.id] || 'unknown';
|
|
1964
|
+
const category = this.getStateCategory(state);
|
|
1965
|
+
return category === this.filters.status;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Filter by time range
|
|
1970
|
+
const timeRange = this.getTimeRangeMs(this.filters.timeRange);
|
|
1971
|
+
if (timeRange > 0) {
|
|
1972
|
+
const cutoff = Date.now() - timeRange;
|
|
1973
|
+
filtered = filtered.filter(conv => {
|
|
1974
|
+
const lastModified = new Date(conv.lastModified).getTime();
|
|
1975
|
+
return lastModified >= cutoff;
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// Filter by search
|
|
1980
|
+
if (this.filters.search) {
|
|
1981
|
+
const searchLower = this.filters.search.toLowerCase();
|
|
1982
|
+
filtered = filtered.filter(conv => {
|
|
1983
|
+
return (conv.title || '').toLowerCase().includes(searchLower) ||
|
|
1984
|
+
(conv.project || '').toLowerCase().includes(searchLower) ||
|
|
1985
|
+
(conv.lastMessage || '').toLowerCase().includes(searchLower);
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return filtered;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Get time range in milliseconds
|
|
1994
|
+
* @param {string} range - Time range string
|
|
1995
|
+
* @returns {number} Milliseconds
|
|
1996
|
+
*/
|
|
1997
|
+
getTimeRangeMs(range) {
|
|
1998
|
+
const ranges = {
|
|
1999
|
+
'1h': 60 * 60 * 1000,
|
|
2000
|
+
'24h': 24 * 60 * 60 * 1000,
|
|
2001
|
+
'7d': 7 * 24 * 60 * 60 * 1000,
|
|
2002
|
+
'30d': 30 * 24 * 60 * 60 * 1000
|
|
2003
|
+
};
|
|
2004
|
+
return ranges[range] || 0;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Get state category for filtering
|
|
2009
|
+
* @param {string} state - Detailed conversation state
|
|
2010
|
+
* @returns {string} Category: 'active' or 'inactive'
|
|
2011
|
+
*/
|
|
2012
|
+
getStateCategory(state) {
|
|
2013
|
+
// Active states - conversation is currently being used or recently active
|
|
2014
|
+
const activeStates = [
|
|
2015
|
+
'Claude Code working...',
|
|
2016
|
+
'Awaiting user input...',
|
|
2017
|
+
'User typing...',
|
|
2018
|
+
'Awaiting response...',
|
|
2019
|
+
'Recently active'
|
|
2020
|
+
];
|
|
2021
|
+
|
|
2022
|
+
// Inactive states - conversation is idle or old
|
|
2023
|
+
const inactiveStates = [
|
|
2024
|
+
'Idle',
|
|
2025
|
+
'Inactive',
|
|
2026
|
+
'Old',
|
|
2027
|
+
'unknown'
|
|
2028
|
+
];
|
|
2029
|
+
|
|
2030
|
+
if (activeStates.includes(state)) {
|
|
2031
|
+
return 'active';
|
|
2032
|
+
} else if (inactiveStates.includes(state)) {
|
|
2033
|
+
return 'inactive';
|
|
2034
|
+
} else {
|
|
2035
|
+
// Default for any unknown states
|
|
2036
|
+
return 'inactive';
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* Get simple conversation preview text (avoids repeating metadata)
|
|
2042
|
+
* @param {Object} conv - Conversation object
|
|
2043
|
+
* @returns {string} Preview text
|
|
2044
|
+
*/
|
|
2045
|
+
getSimpleConversationPreview(conv) {
|
|
2046
|
+
// If we have a last message, show it (this is the most useful info)
|
|
2047
|
+
if (conv.lastMessage && conv.lastMessage.trim()) {
|
|
2048
|
+
const lastMsg = conv.lastMessage.trim();
|
|
2049
|
+
|
|
2050
|
+
// Check if last message is a simple confirmation and try to make it more descriptive
|
|
2051
|
+
if (this.isSimpleConfirmation(lastMsg)) {
|
|
2052
|
+
const messageCount = conv.messageCount || 0;
|
|
2053
|
+
const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'recently';
|
|
2054
|
+
return `User confirmed action • ${messageCount} messages • ${lastActivity}`;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Check if it's a tool-related message
|
|
2058
|
+
if (lastMsg.includes('Tool Result') || lastMsg.includes('[Tool:')) {
|
|
2059
|
+
return `Tool execution completed • ${this.truncateText(lastMsg, 60)}`;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
return this.truncateText(lastMsg, 80);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// For empty conversations, show descriptive text
|
|
2066
|
+
const messageCount = conv.messageCount || 0;
|
|
2067
|
+
if (messageCount === 0) {
|
|
2068
|
+
return 'Empty conversation - click to start chatting';
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// For conversations without lastMessage but with messages, show informative text
|
|
2072
|
+
const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'unknown';
|
|
2073
|
+
return `${messageCount} messages • Last activity ${lastActivity}`;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Check if a message is a simple confirmation
|
|
2078
|
+
* @param {string} message - Message content
|
|
2079
|
+
* @returns {boolean} True if it's a simple confirmation
|
|
2080
|
+
*/
|
|
2081
|
+
isSimpleConfirmation(message) {
|
|
2082
|
+
const trimmed = message.trim();
|
|
2083
|
+
const confirmationPatterns = [
|
|
2084
|
+
/^\[ok\]$/i,
|
|
2085
|
+
/^ok$/i,
|
|
2086
|
+
/^yes$/i,
|
|
2087
|
+
/^\[yes\]$/i,
|
|
2088
|
+
/^y$/i,
|
|
2089
|
+
/^\[y\]$/i,
|
|
2090
|
+
/^[1-9]$/, // Choice selection
|
|
2091
|
+
/^(continue|proceed|accept|confirm|done)$/i
|
|
2092
|
+
];
|
|
2093
|
+
|
|
2094
|
+
return confirmationPatterns.some(pattern => pattern.test(trimmed));
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Get conversation preview text (legacy method - still used in other places)
|
|
2099
|
+
* @param {Object} conv - Conversation object
|
|
2100
|
+
* @param {string} state - Conversation state
|
|
2101
|
+
* @returns {string} Preview text
|
|
2102
|
+
*/
|
|
2103
|
+
getConversationPreview(conv, state) {
|
|
2104
|
+
// If we have a last message, show it
|
|
2105
|
+
if (conv.lastMessage && conv.lastMessage.trim()) {
|
|
2106
|
+
return this.truncateText(conv.lastMessage, 60);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Otherwise, show conversation info based on state and metadata
|
|
2110
|
+
const messageCount = conv.messageCount || 0;
|
|
2111
|
+
|
|
2112
|
+
if (messageCount === 0) {
|
|
2113
|
+
return `Empty conversation • Project: ${conv.project || 'Unknown'}`;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Show state-based preview
|
|
2117
|
+
if (state === 'Claude Code working...') {
|
|
2118
|
+
return `Claude is working • ${messageCount} messages`;
|
|
2119
|
+
} else if (state === 'Awaiting user input...') {
|
|
2120
|
+
return `Waiting for your input • ${messageCount} messages`;
|
|
2121
|
+
} else if (state === 'User typing...') {
|
|
2122
|
+
return `Ready for your message • ${messageCount} messages`;
|
|
2123
|
+
} else if (state === 'Recently active') {
|
|
2124
|
+
return `Recently active • ${messageCount} messages`;
|
|
2125
|
+
} else {
|
|
2126
|
+
return `${messageCount} messages • Last active ${this.formatRelativeTime(new Date(conv.lastModified))}`;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/**
|
|
2131
|
+
* Get state CSS class
|
|
2132
|
+
* @param {string} state - Conversation state
|
|
2133
|
+
* @returns {string} CSS class
|
|
2134
|
+
*/
|
|
2135
|
+
getStateClass(state) {
|
|
2136
|
+
const stateClasses = {
|
|
2137
|
+
'Claude Code working...': 'status-active',
|
|
2138
|
+
'Awaiting user input...': 'status-waiting',
|
|
2139
|
+
'User typing...': 'status-typing',
|
|
2140
|
+
'Awaiting response...': 'status-pending',
|
|
2141
|
+
'Recently active': 'status-recent',
|
|
2142
|
+
'Idle': 'status-idle',
|
|
2143
|
+
'Inactive': 'status-inactive',
|
|
2144
|
+
'Old': 'status-old',
|
|
2145
|
+
'unknown': 'status-unknown'
|
|
2146
|
+
};
|
|
2147
|
+
return stateClasses[state] || 'status-unknown';
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Get state label
|
|
2152
|
+
* @param {string} state - Conversation state
|
|
2153
|
+
* @returns {string} Human readable label
|
|
2154
|
+
*/
|
|
2155
|
+
getStateLabel(state) {
|
|
2156
|
+
const stateLabels = {
|
|
2157
|
+
'Claude Code working...': 'Working',
|
|
2158
|
+
'Awaiting user input...': 'Awaiting input',
|
|
2159
|
+
'User typing...': 'Typing',
|
|
2160
|
+
'Awaiting response...': 'Awaiting response',
|
|
2161
|
+
'Recently active': 'Recent',
|
|
2162
|
+
'Idle': 'Idle',
|
|
2163
|
+
'Inactive': 'Inactive',
|
|
2164
|
+
'Old': 'Old',
|
|
2165
|
+
'unknown': 'Unknown'
|
|
2166
|
+
};
|
|
2167
|
+
return stateLabels[state] || state;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Truncate text to specified length
|
|
2172
|
+
* @param {string} text - Text to truncate
|
|
2173
|
+
* @param {number} maxLength - Maximum length
|
|
2174
|
+
* @returns {string} Truncated text
|
|
2175
|
+
*/
|
|
2176
|
+
truncateText(text, maxLength) {
|
|
2177
|
+
if (!text || text.length <= maxLength) return text;
|
|
2178
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Update filter
|
|
2183
|
+
* @param {string} filterName - Filter name
|
|
2184
|
+
* @param {string} value - Filter value
|
|
2185
|
+
*/
|
|
2186
|
+
updateFilter(filterName, value) {
|
|
2187
|
+
this.filters[filterName] = value;
|
|
2188
|
+
// When filters change, restart from beginning
|
|
2189
|
+
this.refreshFromBeginning();
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Clear search
|
|
2194
|
+
*/
|
|
2195
|
+
clearSearch() {
|
|
2196
|
+
const searchInput = this.container.querySelector('#search-filter');
|
|
2197
|
+
if (!searchInput) return; // Guard against null when AgentsPage isn't rendered
|
|
2198
|
+
|
|
2199
|
+
searchInput.value = '';
|
|
2200
|
+
this.updateFilter('search', '');
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* Clear all filters
|
|
2205
|
+
*/
|
|
2206
|
+
clearAllFilters() {
|
|
2207
|
+
this.filters = {
|
|
2208
|
+
status: 'all',
|
|
2209
|
+
timeRange: '7d',
|
|
2210
|
+
search: ''
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
// Reset UI
|
|
2214
|
+
const statusFilter = this.container.querySelector('#status-filter');
|
|
2215
|
+
const timeFilter = this.container.querySelector('#time-filter');
|
|
2216
|
+
const searchFilter = this.container.querySelector('#search-filter');
|
|
2217
|
+
|
|
2218
|
+
if (statusFilter) statusFilter.value = 'all';
|
|
2219
|
+
if (timeFilter) timeFilter.value = '7d';
|
|
2220
|
+
if (searchFilter) searchFilter.value = '';
|
|
2221
|
+
|
|
2222
|
+
// Restart from beginning when clearing filters
|
|
2223
|
+
this.refreshFromBeginning();
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* Refresh conversations display
|
|
2228
|
+
*/
|
|
2229
|
+
refreshConversationsDisplay() {
|
|
2230
|
+
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2231
|
+
const statesData = this.stateService.getStateProperty('conversationStates') || {};
|
|
2232
|
+
// Extract activeStates from the stored state data
|
|
2233
|
+
const activeStates = statesData?.activeStates || {};
|
|
2234
|
+
this.renderConversationsList(conversations, activeStates);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/**
|
|
2238
|
+
* Refresh from beginning - resets pagination
|
|
2239
|
+
*/
|
|
2240
|
+
async refreshFromBeginning() {
|
|
2241
|
+
// Clear cache
|
|
2242
|
+
this.loadedConversations = [];
|
|
2243
|
+
this.loadedMessages.clear();
|
|
2244
|
+
|
|
2245
|
+
// Reset pagination
|
|
2246
|
+
this.pagination = {
|
|
2247
|
+
currentPage: 0,
|
|
2248
|
+
limit: 10,
|
|
2249
|
+
hasMore: true,
|
|
2250
|
+
isLoading: false
|
|
2251
|
+
};
|
|
2252
|
+
|
|
2253
|
+
// Clear list and reload
|
|
2254
|
+
const listContainer = this.container.querySelector('#conversations-list');
|
|
2255
|
+
if (listContainer) {
|
|
2256
|
+
listContainer.innerHTML = '';
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
await this.loadConversationsData();
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* Refresh conversations data
|
|
2264
|
+
*/
|
|
2265
|
+
async refreshConversations() {
|
|
2266
|
+
const refreshBtn = this.container.querySelector('#refresh-conversations');
|
|
2267
|
+
if (!refreshBtn) return; // Guard against null when AgentsPage isn't rendered
|
|
2268
|
+
|
|
2269
|
+
refreshBtn.disabled = true;
|
|
2270
|
+
const iconElement = refreshBtn.querySelector('.btn-icon');
|
|
2271
|
+
if (iconElement) {
|
|
2272
|
+
iconElement.style.animation = 'spin 1s linear infinite';
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
try {
|
|
2276
|
+
// Clear both server and client cache to force fresh data
|
|
2277
|
+
await this.dataService.clearServerCache('conversations');
|
|
2278
|
+
await this.loadConversationsData();
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
console.error('Error refreshing conversations:', error);
|
|
2281
|
+
this.stateService.setError('Failed to refresh conversations');
|
|
2282
|
+
} finally {
|
|
2283
|
+
refreshBtn.disabled = false;
|
|
2284
|
+
if (iconElement) {
|
|
2285
|
+
iconElement.style.animation = '';
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
/**
|
|
2291
|
+
* Check if there are active filters
|
|
2292
|
+
* @returns {boolean} True if filters are active
|
|
2293
|
+
*/
|
|
2294
|
+
hasActiveFilters() {
|
|
2295
|
+
const searchInput = this.container.querySelector('#conversation-search');
|
|
2296
|
+
const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
|
2297
|
+
|
|
2298
|
+
// Check if search filter is active
|
|
2299
|
+
if (searchTerm) {
|
|
2300
|
+
return true;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// Check if state filters are active
|
|
2304
|
+
const filterButtons = this.container.querySelectorAll('.filter-btn');
|
|
2305
|
+
const activeFilters = Array.from(filterButtons).filter(btn =>
|
|
2306
|
+
btn.classList.contains('active') && btn.getAttribute('data-state') !== 'all'
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
return activeFilters.length > 0;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/**
|
|
2313
|
+
* Update results count
|
|
2314
|
+
* @param {number} count - Number of results
|
|
2315
|
+
* @param {boolean} hasActiveFilters - Whether filters are active
|
|
2316
|
+
*/
|
|
2317
|
+
updateResultsCount(count, hasActiveFilters = false) {
|
|
2318
|
+
// Update main results count
|
|
2319
|
+
const resultsCount = this.container.querySelector('#results-count');
|
|
2320
|
+
if (resultsCount) {
|
|
2321
|
+
let countText = `${count} conversation${count !== 1 ? 's' : ''} found`;
|
|
2322
|
+
if (hasActiveFilters && this.pagination && this.pagination.totalCount && count < this.pagination.totalCount) {
|
|
2323
|
+
countText += ` (filtered from ${this.pagination.totalCount})`;
|
|
2324
|
+
}
|
|
2325
|
+
resultsCount.textContent = countText;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Update sidebar count
|
|
2329
|
+
const sidebarCount = this.container.querySelector('#sidebar-count');
|
|
2330
|
+
if (sidebarCount) {
|
|
2331
|
+
sidebarCount.textContent = count;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* Show empty state
|
|
2337
|
+
*/
|
|
2338
|
+
showEmptyState() {
|
|
2339
|
+
const conversationsList = this.container.querySelector('#conversations-list');
|
|
2340
|
+
const emptyState = this.container.querySelector('#empty-state');
|
|
2341
|
+
if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
|
|
2342
|
+
|
|
2343
|
+
conversationsList.style.display = 'none';
|
|
2344
|
+
emptyState.style.display = 'flex';
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Hide empty state
|
|
2349
|
+
*/
|
|
2350
|
+
hideEmptyState() {
|
|
2351
|
+
const conversationsList = this.container.querySelector('#conversations-list');
|
|
2352
|
+
const emptyState = this.container.querySelector('#empty-state');
|
|
2353
|
+
if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
|
|
2354
|
+
|
|
2355
|
+
conversationsList.style.display = 'block';
|
|
2356
|
+
emptyState.style.display = 'none';
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* Toggle between grid and table view
|
|
2361
|
+
* @param {string} view - View type ('grid' or 'table')
|
|
2362
|
+
*/
|
|
2363
|
+
toggleView(view) {
|
|
2364
|
+
const toggleBtns = this.container.querySelectorAll('.toggle-btn');
|
|
2365
|
+
toggleBtns.forEach(btn => {
|
|
2366
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
const gridElement = this.container.querySelector('#conversations-grid');
|
|
2370
|
+
const tableSection = this.container.querySelector('.conversations-table-section');
|
|
2371
|
+
|
|
2372
|
+
if (!gridElement || !tableSection) return; // Guard against null when AgentsPage isn't rendered
|
|
2373
|
+
|
|
2374
|
+
const gridSection = gridElement.parentNode;
|
|
2375
|
+
|
|
2376
|
+
if (view === 'table') {
|
|
2377
|
+
gridSection.style.display = 'none';
|
|
2378
|
+
tableSection.style.display = 'block';
|
|
2379
|
+
} else {
|
|
2380
|
+
gridSection.style.display = 'block';
|
|
2381
|
+
tableSection.style.display = 'none';
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* View conversation details
|
|
2387
|
+
* @param {string} conversationId - Conversation ID
|
|
2388
|
+
*/
|
|
2389
|
+
viewConversation(conversationId) {
|
|
2390
|
+
// This would open a detailed conversation view
|
|
2391
|
+
// Implementation would show conversation details modal or navigate to detail page
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/**
|
|
2395
|
+
* Export single conversation
|
|
2396
|
+
* @param {string} conversationId - Conversation ID
|
|
2397
|
+
*/
|
|
2398
|
+
exportSingleConversation(conversationId) {
|
|
2399
|
+
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2400
|
+
const conversation = conversations.find(conv => conv.id === conversationId);
|
|
2401
|
+
|
|
2402
|
+
if (conversation) {
|
|
2403
|
+
const dataStr = JSON.stringify(conversation, null, 2);
|
|
2404
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
2405
|
+
const url = URL.createObjectURL(dataBlob);
|
|
2406
|
+
|
|
2407
|
+
const link = document.createElement('a');
|
|
2408
|
+
link.href = url;
|
|
2409
|
+
link.download = `conversation-${conversationId}-${new Date().toISOString().split('T')[0]}.json`;
|
|
2410
|
+
link.click();
|
|
2411
|
+
|
|
2412
|
+
URL.revokeObjectURL(url);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/**
|
|
2417
|
+
* Export all conversations
|
|
2418
|
+
*/
|
|
2419
|
+
exportConversations() {
|
|
2420
|
+
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2421
|
+
const states = this.stateService.getStateProperty('conversationStates') || {};
|
|
2422
|
+
const filteredConversations = this.filterConversations(conversations, states);
|
|
2423
|
+
|
|
2424
|
+
const dataStr = JSON.stringify({
|
|
2425
|
+
conversations: filteredConversations,
|
|
2426
|
+
states: states,
|
|
2427
|
+
exportDate: new Date().toISOString(),
|
|
2428
|
+
filters: this.filters
|
|
2429
|
+
}, null, 2);
|
|
2430
|
+
|
|
2431
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
2432
|
+
const url = URL.createObjectURL(dataBlob);
|
|
2433
|
+
|
|
2434
|
+
const link = document.createElement('a');
|
|
2435
|
+
link.href = url;
|
|
2436
|
+
link.download = `claude-conversations-${new Date().toISOString().split('T')[0]}.json`;
|
|
2437
|
+
link.click();
|
|
2438
|
+
|
|
2439
|
+
URL.revokeObjectURL(url);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Update conversations display
|
|
2444
|
+
* @param {Array} conversations - Conversations data
|
|
2445
|
+
*/
|
|
2446
|
+
updateConversationsDisplay(conversations) {
|
|
2447
|
+
const statesData = this.stateService.getStateProperty('conversationStates') || {};
|
|
2448
|
+
const activeStates = statesData?.activeStates || {};
|
|
2449
|
+
this.renderConversationsList(conversations, activeStates);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Update conversation states
|
|
2454
|
+
* @param {Object} activeStates - Active conversation states (direct object, not nested)
|
|
2455
|
+
*/
|
|
2456
|
+
updateConversationStates(activeStates) {
|
|
2457
|
+
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
// Re-render conversation list with new states
|
|
2461
|
+
this.renderConversationsList(conversations, activeStates || {});
|
|
2462
|
+
|
|
2463
|
+
// Update banner if we have a selected conversation
|
|
2464
|
+
if (this.selectedConversationId && activeStates && activeStates[this.selectedConversationId]) {
|
|
2465
|
+
this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* Handle conversation state change
|
|
2471
|
+
* @param {Object} _state - New state (unused but required by interface)
|
|
2472
|
+
*/
|
|
2473
|
+
handleConversationStateChange(_state) {
|
|
2474
|
+
this.refreshConversationsDisplay();
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Update loading state
|
|
2479
|
+
* @param {boolean} isLoading - Loading state
|
|
2480
|
+
*/
|
|
2481
|
+
updateLoadingState(isLoading) {
|
|
2482
|
+
const loadingState = this.container.querySelector('#conversations-loading');
|
|
2483
|
+
if (loadingState) {
|
|
2484
|
+
loadingState.style.display = isLoading ? 'flex' : 'none';
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Update error state
|
|
2490
|
+
* @param {Error|string} error - Error object or message
|
|
2491
|
+
*/
|
|
2492
|
+
updateErrorState(error) {
|
|
2493
|
+
const errorState = this.container.querySelector('#conversations-error');
|
|
2494
|
+
const errorMessage = this.container.querySelector('.error-message');
|
|
2495
|
+
|
|
2496
|
+
if (errorState && errorMessage) {
|
|
2497
|
+
if (error) {
|
|
2498
|
+
errorMessage.textContent = error.message || error;
|
|
2499
|
+
errorState.style.display = 'flex';
|
|
2500
|
+
} else {
|
|
2501
|
+
errorState.style.display = 'none';
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* Destroy agents page
|
|
2508
|
+
*/
|
|
2509
|
+
destroy() {
|
|
2510
|
+
// Cleanup components
|
|
2511
|
+
Object.values(this.components).forEach(component => {
|
|
2512
|
+
if (component.destroy) {
|
|
2513
|
+
component.destroy();
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
// Cleanup scroll listeners
|
|
2518
|
+
const messagesContent = this.container.querySelector('#messages-content');
|
|
2519
|
+
if (messagesContent && this.messagesScrollListener) {
|
|
2520
|
+
messagesContent.removeEventListener('scroll', this.messagesScrollListener);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// Unsubscribe from state changes
|
|
2524
|
+
if (this.unsubscribe) {
|
|
2525
|
+
this.unsubscribe();
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
this.isInitialized = false;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// Export for module use
|
|
2533
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
2534
|
+
module.exports = AgentsPage;
|
|
2535
|
+
}
|