claude-code-templates 1.16.1 → 1.17.1
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 +7 -7
- package/bin/create-claude-config.js +17 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +239 -36
- package/src/health-check.js +310 -0
- package/src/index.js +1256 -36
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- package/templates/rust/README.md +0 -26
|
@@ -1,4761 +0,0 @@
|
|
|
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
|
-
// Initialize header component
|
|
20
|
-
this.headerComponent = null;
|
|
21
|
-
|
|
22
|
-
// Pagination state for conversations
|
|
23
|
-
this.pagination = {
|
|
24
|
-
currentPage: 0,
|
|
25
|
-
limit: 10,
|
|
26
|
-
hasMore: true,
|
|
27
|
-
isLoading: false
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Pagination state for messages
|
|
31
|
-
this.messagesPagination = {
|
|
32
|
-
currentPage: 0,
|
|
33
|
-
limit: 10,
|
|
34
|
-
hasMore: true,
|
|
35
|
-
isLoading: false,
|
|
36
|
-
conversationId: null
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Loaded conversations cache
|
|
40
|
-
this.loadedConversations = [];
|
|
41
|
-
this.loadedMessages = new Map(); // Cache messages by conversation ID (now stores paginated data)
|
|
42
|
-
|
|
43
|
-
// Agent data
|
|
44
|
-
this.agents = [];
|
|
45
|
-
this.selectedAgentId = null;
|
|
46
|
-
|
|
47
|
-
// State transition tracking for enhanced user experience
|
|
48
|
-
this.lastMessageTime = new Map(); // Track when last message was received per conversation
|
|
49
|
-
|
|
50
|
-
// Initialize tool display component
|
|
51
|
-
this.toolDisplay = new ToolDisplay();
|
|
52
|
-
|
|
53
|
-
// Subscribe to state changes
|
|
54
|
-
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
|
|
55
|
-
|
|
56
|
-
// Subscribe to DataService events for real-time updates
|
|
57
|
-
this.dataService.addEventListener((type, data) => {
|
|
58
|
-
if (type === 'new_message') {
|
|
59
|
-
console.log('🔄 WebSocket: New message received', { conversationId: data.conversationId });
|
|
60
|
-
this.handleNewMessage(data.conversationId, data.message, data.metadata);
|
|
61
|
-
} else if (type === 'console_interaction') {
|
|
62
|
-
console.log('🔄 WebSocket: Console interaction request received', data);
|
|
63
|
-
this.showConsoleInteraction(data);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Initialize the agents page
|
|
70
|
-
*/
|
|
71
|
-
async initialize() {
|
|
72
|
-
if (this.isInitialized) return;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
this.stateService.setLoading(true);
|
|
76
|
-
await this.render();
|
|
77
|
-
await this.initializeComponents();
|
|
78
|
-
await this.loadAgentsData();
|
|
79
|
-
await this.loadConversationsData();
|
|
80
|
-
this.isInitialized = true;
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error('Error initializing agents page:', error);
|
|
83
|
-
this.stateService.setError(error);
|
|
84
|
-
} finally {
|
|
85
|
-
this.stateService.setLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Handle state changes from StateService (WebSocket updates)
|
|
91
|
-
* @param {Object} state - New state
|
|
92
|
-
* @param {string} action - Action that caused the change
|
|
93
|
-
*/
|
|
94
|
-
handleStateChange(state, action) {
|
|
95
|
-
switch (action) {
|
|
96
|
-
case 'update_conversations':
|
|
97
|
-
// Don't replace loaded conversations, just update states
|
|
98
|
-
break;
|
|
99
|
-
case 'update_conversation_states':
|
|
100
|
-
console.log('🔄 WebSocket: Conversation states updated', { count: Object.keys(state.conversationStates?.activeStates || state.conversationStates || {}).length });
|
|
101
|
-
|
|
102
|
-
// Handle both direct states object and nested structure
|
|
103
|
-
const activeStates = state.conversationStates?.activeStates || state.conversationStates || {};
|
|
104
|
-
|
|
105
|
-
this.updateConversationStates(activeStates);
|
|
106
|
-
break;
|
|
107
|
-
case 'set_loading':
|
|
108
|
-
this.updateLoadingState(state.isLoading);
|
|
109
|
-
break;
|
|
110
|
-
case 'set_error':
|
|
111
|
-
this.updateErrorState(state.error);
|
|
112
|
-
break;
|
|
113
|
-
case 'conversation_state_change':
|
|
114
|
-
this.handleConversationStateChange(state);
|
|
115
|
-
break;
|
|
116
|
-
case 'data_refresh':
|
|
117
|
-
// On real-time data refresh, update conversation states but keep pagination
|
|
118
|
-
this.updateConversationStatesOnly();
|
|
119
|
-
break;
|
|
120
|
-
case 'new_message':
|
|
121
|
-
// Handle new message in real-time
|
|
122
|
-
this.handleNewMessage(state.conversationId, state.message, state.metadata);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Handle new message received via WebSocket
|
|
129
|
-
* @param {string} conversationId - Conversation ID that received new message
|
|
130
|
-
* @param {Object} message - New message object
|
|
131
|
-
* @param {Object} metadata - Additional metadata
|
|
132
|
-
*/
|
|
133
|
-
handleNewMessage(conversationId, message, metadata) {
|
|
134
|
-
// Log essential message info for debugging
|
|
135
|
-
console.log('🔄 WebSocket: Processing new message', {
|
|
136
|
-
conversationId,
|
|
137
|
-
role: message?.role,
|
|
138
|
-
hasTools: Array.isArray(message?.content) ? message.content.some(b => b.type === 'tool_use') : false,
|
|
139
|
-
hasToolResults: !!message?.toolResults
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Always update the message cache for this conversation
|
|
143
|
-
const existingMessages = this.loadedMessages.get(conversationId) || [];
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Track message timing for better state transitions
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
this.lastMessageTime.set(conversationId, now);
|
|
149
|
-
|
|
150
|
-
// IMMEDIATE STATE TRANSITION based on message appearance
|
|
151
|
-
if (this.selectedConversationId === conversationId) {
|
|
152
|
-
if (message?.role === 'user') {
|
|
153
|
-
// User message just appeared - Claude immediately starts working
|
|
154
|
-
console.log('⚡ User message detected - Claude starting work immediately');
|
|
155
|
-
this.updateStateBanner(conversationId, 'Claude Code working...');
|
|
156
|
-
} else if (message?.role === 'assistant') {
|
|
157
|
-
// Assistant message appeared - analyze for specific state
|
|
158
|
-
const intelligentState = this.analyzeMessageForState(message, existingMessages);
|
|
159
|
-
console.log(`🤖 Assistant message detected - state: ${intelligentState}`);
|
|
160
|
-
this.updateStateBanner(conversationId, intelligentState);
|
|
161
|
-
|
|
162
|
-
// No additional timeout needed - state is determined by message content
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Check if we already have this message (avoid duplicates)
|
|
167
|
-
const messageExists = existingMessages.some(msg =>
|
|
168
|
-
msg.id === message.id ||
|
|
169
|
-
(msg.timestamp === message.timestamp && msg.role === message.role)
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
if (!messageExists) {
|
|
173
|
-
// Add new message to the end
|
|
174
|
-
const updatedMessages = [...existingMessages, message];
|
|
175
|
-
this.loadedMessages.set(conversationId, updatedMessages);
|
|
176
|
-
|
|
177
|
-
// Refresh only the conversation states to show updated status/timestamp
|
|
178
|
-
// Don't do full reload as it can interfere with message cache
|
|
179
|
-
this.updateConversationStatesOnly();
|
|
180
|
-
|
|
181
|
-
// If this conversation is currently selected, update the messages view
|
|
182
|
-
if (this.selectedConversationId === conversationId) {
|
|
183
|
-
// Re-render messages with new message
|
|
184
|
-
this.renderCachedMessages(updatedMessages, false);
|
|
185
|
-
|
|
186
|
-
// Auto-scroll to new message
|
|
187
|
-
this.scrollToBottom();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Show notification
|
|
191
|
-
this.showNewMessageNotification(message, metadata);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Update only conversation states without affecting pagination
|
|
197
|
-
*/
|
|
198
|
-
async updateConversationStatesOnly() {
|
|
199
|
-
try {
|
|
200
|
-
const statesData = await this.dataService.getConversationStates();
|
|
201
|
-
const activeStates = statesData?.activeStates || {};
|
|
202
|
-
|
|
203
|
-
// Update StateService with fresh states
|
|
204
|
-
this.stateService.updateConversationStates(activeStates);
|
|
205
|
-
|
|
206
|
-
// Update states in already loaded conversations
|
|
207
|
-
this.updateConversationStateElements(activeStates);
|
|
208
|
-
|
|
209
|
-
// Update banner if we have a selected conversation
|
|
210
|
-
if (this.selectedConversationId && activeStates[this.selectedConversationId]) {
|
|
211
|
-
this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
} catch (error) {
|
|
215
|
-
console.error('Error updating conversation states:', error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Analyze a message to determine intelligent conversation state
|
|
221
|
-
* @param {Object} message - The message to analyze
|
|
222
|
-
* @param {Array} existingMessages - Previous messages in conversation
|
|
223
|
-
* @returns {string} Intelligent state description
|
|
224
|
-
*/
|
|
225
|
-
analyzeMessageForState(message, existingMessages = []) {
|
|
226
|
-
const role = message?.role;
|
|
227
|
-
const content = message?.content;
|
|
228
|
-
const hasToolResults = !!message?.toolResults && message.toolResults.length > 0;
|
|
229
|
-
const messageTime = new Date(message?.timestamp || Date.now());
|
|
230
|
-
const now = new Date();
|
|
231
|
-
const messageAge = (now - messageTime) / 1000; // seconds
|
|
232
|
-
|
|
233
|
-
if (role === 'assistant') {
|
|
234
|
-
// Analyze assistant messages with enhanced logic
|
|
235
|
-
if (Array.isArray(content)) {
|
|
236
|
-
const hasToolUse = content.some(block => block.type === 'tool_use');
|
|
237
|
-
const hasText = content.some(block => block.type === 'text');
|
|
238
|
-
const textBlocks = content.filter(block => block.type === 'text');
|
|
239
|
-
const toolUseBlocks = content.filter(block => block.type === 'tool_use');
|
|
240
|
-
|
|
241
|
-
// Enhanced tool execution detection with immediate response
|
|
242
|
-
if (hasToolUse) {
|
|
243
|
-
const toolNames = toolUseBlocks.map(tool => tool.name).join(', ');
|
|
244
|
-
|
|
245
|
-
if (!hasToolResults) {
|
|
246
|
-
// Tool just sent - immediate execution state
|
|
247
|
-
console.log(`🔧 Tools detected: ${toolNames} - showing execution state`);
|
|
248
|
-
|
|
249
|
-
if (toolNames.includes('bash') || toolNames.includes('edit') || toolNames.includes('write') || toolNames.includes('multiedit')) {
|
|
250
|
-
return 'Executing tools...';
|
|
251
|
-
} else if (toolNames.includes('read') || toolNames.includes('grep') || toolNames.includes('glob') || toolNames.includes('task')) {
|
|
252
|
-
return 'Analyzing code...';
|
|
253
|
-
} else if (toolNames.includes('webfetch') || toolNames.includes('websearch')) {
|
|
254
|
-
return 'Fetching data...';
|
|
255
|
-
}
|
|
256
|
-
return 'Awaiting tool response...';
|
|
257
|
-
} else {
|
|
258
|
-
// Has tool results - Claude is processing them
|
|
259
|
-
console.log(`📊 Tools completed: ${toolNames} - analyzing results`);
|
|
260
|
-
return 'Analyzing results...';
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Enhanced text analysis
|
|
265
|
-
if (hasText) {
|
|
266
|
-
const textContent = textBlocks.map(block => block.text).join(' ').toLowerCase();
|
|
267
|
-
|
|
268
|
-
// Working indicators
|
|
269
|
-
if (textContent.includes('let me') ||
|
|
270
|
-
textContent.includes('i\'ll') ||
|
|
271
|
-
textContent.includes('i will') ||
|
|
272
|
-
textContent.includes('i\'m going to') ||
|
|
273
|
-
textContent.includes('let\'s') ||
|
|
274
|
-
textContent.includes('first, i\'ll') ||
|
|
275
|
-
textContent.includes('now i\'ll')) {
|
|
276
|
-
return 'Claude Code working...';
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Analysis indicators
|
|
280
|
-
if (textContent.includes('analyzing') ||
|
|
281
|
-
textContent.includes('examining') ||
|
|
282
|
-
textContent.includes('looking at') ||
|
|
283
|
-
textContent.includes('reviewing')) {
|
|
284
|
-
return 'Analyzing code...';
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Completion indicators
|
|
288
|
-
if (textContent.includes('completed') ||
|
|
289
|
-
textContent.includes('finished') ||
|
|
290
|
-
textContent.includes('done') ||
|
|
291
|
-
textContent.includes('successfully')) {
|
|
292
|
-
return 'Task completed';
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// User input needed - enhanced detection
|
|
296
|
-
if (textContent.endsWith('?') ||
|
|
297
|
-
textContent.includes('what would you like') ||
|
|
298
|
-
textContent.includes('how can i help') ||
|
|
299
|
-
textContent.includes('would you like me to') ||
|
|
300
|
-
textContent.includes('should i') ||
|
|
301
|
-
textContent.includes('do you want') ||
|
|
302
|
-
textContent.includes('let me know') ||
|
|
303
|
-
textContent.includes('please let me know') ||
|
|
304
|
-
textContent.includes('what do you think') ||
|
|
305
|
-
textContent.includes('any questions')) {
|
|
306
|
-
return 'Waiting for your response';
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Error/problem indicators
|
|
310
|
-
if (textContent.includes('error') ||
|
|
311
|
-
textContent.includes('failed') ||
|
|
312
|
-
textContent.includes('problem') ||
|
|
313
|
-
textContent.includes('issue')) {
|
|
314
|
-
return 'Encountered issue';
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Recent assistant message suggests waiting for user
|
|
320
|
-
if (messageAge < 300) { // Extended to 5 minutes
|
|
321
|
-
return 'Waiting for your response';
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Default for older assistant messages
|
|
325
|
-
return 'Idle';
|
|
326
|
-
|
|
327
|
-
} else if (role === 'user') {
|
|
328
|
-
// User just sent a message - Claude should be processing
|
|
329
|
-
if (messageAge < 10) {
|
|
330
|
-
return 'Claude Code working...';
|
|
331
|
-
} else if (messageAge < 60) {
|
|
332
|
-
return 'Awaiting response...';
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Older user messages suggest Claude might be working on something complex
|
|
336
|
-
return 'Processing request...';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Enhanced timing analysis
|
|
340
|
-
const lastMessage = existingMessages[existingMessages.length - 1];
|
|
341
|
-
if (lastMessage) {
|
|
342
|
-
const timeSinceLastMessage = Date.now() - new Date(lastMessage.timestamp).getTime();
|
|
343
|
-
|
|
344
|
-
if (timeSinceLastMessage < 30000) { // Less than 30 seconds
|
|
345
|
-
return lastMessage.role === 'user' ? 'Claude Code working...' : 'Recently active';
|
|
346
|
-
} else if (timeSinceLastMessage < 180000) { // Less than 3 minutes
|
|
347
|
-
return 'Idle';
|
|
348
|
-
} else if (timeSinceLastMessage < 1800000) { // Less than 30 minutes
|
|
349
|
-
return 'Waiting for your response';
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return 'Inactive';
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Show console interaction panel for Yes/No prompts
|
|
359
|
-
* @param {Object} interactionData - Interaction data from Claude Code
|
|
360
|
-
*/
|
|
361
|
-
showConsoleInteraction(interactionData) {
|
|
362
|
-
const panel = this.container.querySelector('#console-interaction-panel');
|
|
363
|
-
const description = this.container.querySelector('#interaction-description');
|
|
364
|
-
const prompt = this.container.querySelector('#interaction-prompt');
|
|
365
|
-
const choices = this.container.querySelector('#interaction-choices');
|
|
366
|
-
const textInput = this.container.querySelector('#interaction-text-input');
|
|
367
|
-
|
|
368
|
-
// Show the panel
|
|
369
|
-
panel.style.display = 'block';
|
|
370
|
-
|
|
371
|
-
// Set up the interaction content
|
|
372
|
-
if (interactionData.description) {
|
|
373
|
-
description.innerHTML = `
|
|
374
|
-
<div class="tool-action">
|
|
375
|
-
<strong>${interactionData.tool || 'Action'}:</strong>
|
|
376
|
-
<div class="tool-details">${interactionData.description}</div>
|
|
377
|
-
</div>
|
|
378
|
-
`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (interactionData.prompt) {
|
|
382
|
-
prompt.textContent = interactionData.prompt;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Handle different interaction types
|
|
386
|
-
if (interactionData.type === 'choice' && interactionData.options) {
|
|
387
|
-
// Show multiple choice options
|
|
388
|
-
choices.style.display = 'block';
|
|
389
|
-
textInput.style.display = 'none';
|
|
390
|
-
|
|
391
|
-
const choicesHtml = interactionData.options.map((option, index) => `
|
|
392
|
-
<label class="interaction-choice">
|
|
393
|
-
<input type="radio" name="console-choice" value="${index}" ${index === 0 ? 'checked' : ''}>
|
|
394
|
-
<span class="choice-number">${index + 1}.</span>
|
|
395
|
-
<span class="choice-text">${option}</span>
|
|
396
|
-
</label>
|
|
397
|
-
`).join('');
|
|
398
|
-
|
|
399
|
-
choices.innerHTML = choicesHtml;
|
|
400
|
-
|
|
401
|
-
} else if (interactionData.type === 'text') {
|
|
402
|
-
// Show text input
|
|
403
|
-
choices.style.display = 'none';
|
|
404
|
-
textInput.style.display = 'block';
|
|
405
|
-
|
|
406
|
-
const textarea = this.container.querySelector('#console-text-input');
|
|
407
|
-
textarea.focus();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Store interaction data for submission
|
|
411
|
-
this.currentInteraction = interactionData;
|
|
412
|
-
|
|
413
|
-
// Bind event listeners
|
|
414
|
-
this.bindInteractionEvents();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Hide console interaction panel
|
|
419
|
-
*/
|
|
420
|
-
hideConsoleInteraction() {
|
|
421
|
-
const panel = this.container.querySelector('#console-interaction-panel');
|
|
422
|
-
panel.style.display = 'none';
|
|
423
|
-
this.currentInteraction = null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Bind event listeners for console interaction
|
|
428
|
-
*/
|
|
429
|
-
bindInteractionEvents() {
|
|
430
|
-
const submitBtn = this.container.querySelector('#interaction-submit');
|
|
431
|
-
const cancelBtn = this.container.querySelector('#interaction-cancel');
|
|
432
|
-
|
|
433
|
-
// Remove existing listeners
|
|
434
|
-
submitBtn.replaceWith(submitBtn.cloneNode(true));
|
|
435
|
-
cancelBtn.replaceWith(cancelBtn.cloneNode(true));
|
|
436
|
-
|
|
437
|
-
// Get fresh references
|
|
438
|
-
const newSubmitBtn = this.container.querySelector('#interaction-submit');
|
|
439
|
-
const newCancelBtn = this.container.querySelector('#interaction-cancel');
|
|
440
|
-
|
|
441
|
-
newSubmitBtn.addEventListener('click', () => this.handleInteractionSubmit());
|
|
442
|
-
newCancelBtn.addEventListener('click', () => this.handleInteractionCancel());
|
|
443
|
-
|
|
444
|
-
// Handle Enter key for text input
|
|
445
|
-
const textarea = this.container.querySelector('#console-text-input');
|
|
446
|
-
if (textarea) {
|
|
447
|
-
textarea.addEventListener('keydown', (e) => {
|
|
448
|
-
if (e.key === 'Enter' && e.ctrlKey) {
|
|
449
|
-
this.handleInteractionSubmit();
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Handle interaction submission
|
|
457
|
-
*/
|
|
458
|
-
async handleInteractionSubmit() {
|
|
459
|
-
if (!this.currentInteraction) return;
|
|
460
|
-
|
|
461
|
-
let response;
|
|
462
|
-
|
|
463
|
-
if (this.currentInteraction.type === 'choice') {
|
|
464
|
-
const selectedChoice = this.container.querySelector('input[name="console-choice"]:checked');
|
|
465
|
-
if (selectedChoice) {
|
|
466
|
-
response = {
|
|
467
|
-
type: 'choice',
|
|
468
|
-
value: parseInt(selectedChoice.value),
|
|
469
|
-
text: this.currentInteraction.options[selectedChoice.value]
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
} else if (this.currentInteraction.type === 'text') {
|
|
473
|
-
const textarea = this.container.querySelector('#console-text-input');
|
|
474
|
-
response = {
|
|
475
|
-
type: 'text',
|
|
476
|
-
value: textarea.value.trim()
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (response) {
|
|
481
|
-
// Send response via WebSocket
|
|
482
|
-
try {
|
|
483
|
-
await this.sendConsoleResponse(this.currentInteraction.id, response);
|
|
484
|
-
console.log('🔄 WebSocket: Console interaction response sent', { id: this.currentInteraction.id, response });
|
|
485
|
-
this.hideConsoleInteraction();
|
|
486
|
-
} catch (error) {
|
|
487
|
-
console.error('Error sending console response:', error);
|
|
488
|
-
// Show error in UI
|
|
489
|
-
this.showInteractionError('Failed to send response. Please try again.');
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Handle interaction cancellation
|
|
496
|
-
*/
|
|
497
|
-
async handleInteractionCancel() {
|
|
498
|
-
if (!this.currentInteraction) return;
|
|
499
|
-
|
|
500
|
-
try {
|
|
501
|
-
await this.sendConsoleResponse(this.currentInteraction.id, { type: 'cancel' });
|
|
502
|
-
console.log('🔄 WebSocket: Console interaction cancelled', { id: this.currentInteraction.id });
|
|
503
|
-
this.hideConsoleInteraction();
|
|
504
|
-
} catch (error) {
|
|
505
|
-
console.error('Error cancelling console interaction:', error);
|
|
506
|
-
this.hideConsoleInteraction(); // Hide anyway on cancel
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Send console response via WebSocket
|
|
512
|
-
* @param {string} interactionId - Interaction ID
|
|
513
|
-
* @param {Object} response - Response data
|
|
514
|
-
*/
|
|
515
|
-
async sendConsoleResponse(interactionId, response) {
|
|
516
|
-
// Send through DataService which will route to WebSocket
|
|
517
|
-
if (this.dataService && this.dataService.webSocketService) {
|
|
518
|
-
this.dataService.webSocketService.send({
|
|
519
|
-
type: 'console_response',
|
|
520
|
-
data: {
|
|
521
|
-
interactionId,
|
|
522
|
-
response
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
} else {
|
|
526
|
-
throw new Error('WebSocket service not available');
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Show error in interaction panel
|
|
532
|
-
* @param {string} message - Error message
|
|
533
|
-
*/
|
|
534
|
-
showInteractionError(message) {
|
|
535
|
-
const panel = this.container.querySelector('#console-interaction-panel');
|
|
536
|
-
const existingError = panel.querySelector('.interaction-error');
|
|
537
|
-
|
|
538
|
-
if (existingError) {
|
|
539
|
-
existingError.remove();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const errorDiv = document.createElement('div');
|
|
543
|
-
errorDiv.className = 'interaction-error';
|
|
544
|
-
errorDiv.textContent = message;
|
|
545
|
-
|
|
546
|
-
const content = panel.querySelector('.interaction-content');
|
|
547
|
-
content.insertBefore(errorDiv, content.querySelector('.interaction-actions'));
|
|
548
|
-
|
|
549
|
-
// Remove error after 5 seconds
|
|
550
|
-
setTimeout(() => {
|
|
551
|
-
if (errorDiv.parentNode) {
|
|
552
|
-
errorDiv.remove();
|
|
553
|
-
}
|
|
554
|
-
}, 5000);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Update conversation state elements in the DOM
|
|
560
|
-
* @param {Object} activeStates - Active conversation states
|
|
561
|
-
*/
|
|
562
|
-
updateConversationStateElements(activeStates) {
|
|
563
|
-
const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
|
|
564
|
-
|
|
565
|
-
conversationItems.forEach(item => {
|
|
566
|
-
const conversationId = item.dataset.id;
|
|
567
|
-
const state = activeStates[conversationId] || 'unknown';
|
|
568
|
-
const stateClass = this.getStateClass(state);
|
|
569
|
-
const stateLabel = this.getStateLabel(state);
|
|
570
|
-
|
|
571
|
-
// Update status dot
|
|
572
|
-
const statusDot = item.querySelector('.status-dot');
|
|
573
|
-
if (statusDot) {
|
|
574
|
-
statusDot.className = `status-dot ${stateClass}`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Update status badge
|
|
578
|
-
const statusBadge = item.querySelector('.sidebar-conversation-badge');
|
|
579
|
-
if (statusBadge) {
|
|
580
|
-
statusBadge.className = `sidebar-conversation-badge ${stateClass}`;
|
|
581
|
-
statusBadge.textContent = stateLabel;
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Render the agents page structure
|
|
588
|
-
*/
|
|
589
|
-
async render() {
|
|
590
|
-
this.container.innerHTML = `
|
|
591
|
-
<div class="agents-page">
|
|
592
|
-
<!-- Page Header (will be replaced by HeaderComponent) -->
|
|
593
|
-
<div id="agents-header-container"></div>
|
|
594
|
-
|
|
595
|
-
<!-- Filters Section -->
|
|
596
|
-
<div class="conversations-filters">
|
|
597
|
-
<div class="filters-row">
|
|
598
|
-
<div class="filter-group">
|
|
599
|
-
<label class="filter-label">Status:</label>
|
|
600
|
-
<select class="filter-select" id="status-filter">
|
|
601
|
-
<option value="all">All</option>
|
|
602
|
-
<option value="active">Active</option>
|
|
603
|
-
<option value="inactive">Inactive</option>
|
|
604
|
-
</select>
|
|
605
|
-
</div>
|
|
606
|
-
|
|
607
|
-
<div class="filter-group">
|
|
608
|
-
<label class="filter-label">Time Range:</label>
|
|
609
|
-
<select class="filter-select" id="time-filter">
|
|
610
|
-
<option value="1h">Last Hour</option>
|
|
611
|
-
<option value="24h">Last 24 Hours</option>
|
|
612
|
-
<option value="7d" selected>Last 7 Days</option>
|
|
613
|
-
<option value="30d">Last 30 Days</option>
|
|
614
|
-
</select>
|
|
615
|
-
</div>
|
|
616
|
-
|
|
617
|
-
<div class="filter-group search-group">
|
|
618
|
-
<label class="filter-label">Search:</label>
|
|
619
|
-
<div class="search-input-container">
|
|
620
|
-
<input type="text" class="filter-input search-input" id="search-filter" placeholder="Search conversations, projects, or messages...">
|
|
621
|
-
<button class="search-clear" id="clear-search" title="Clear search">×</button>
|
|
622
|
-
</div>
|
|
623
|
-
</div>
|
|
624
|
-
</div>
|
|
625
|
-
</div>
|
|
626
|
-
|
|
627
|
-
<!-- Agents Section -->
|
|
628
|
-
<div class="agents-section">
|
|
629
|
-
<div class="agents-header">
|
|
630
|
-
<h4>Available Agents</h4>
|
|
631
|
-
<div class="agents-info">
|
|
632
|
-
<span class="agents-count" id="agents-count">0 agents</span>
|
|
633
|
-
<button class="refresh-agents-btn" id="refresh-agents" title="Refresh agents">
|
|
634
|
-
<span class="btn-icon">🔄</span>
|
|
635
|
-
</button>
|
|
636
|
-
</div>
|
|
637
|
-
</div>
|
|
638
|
-
|
|
639
|
-
<div class="agents-list" id="agents-list">
|
|
640
|
-
<!-- Agent items will be rendered here -->
|
|
641
|
-
</div>
|
|
642
|
-
|
|
643
|
-
<!-- Loading state for agents -->
|
|
644
|
-
<div class="agents-loading" id="agents-loading" style="display: none;">
|
|
645
|
-
<div class="loading-spinner"></div>
|
|
646
|
-
<span class="loading-text">Loading agents...</span>
|
|
647
|
-
</div>
|
|
648
|
-
|
|
649
|
-
<!-- Empty state for agents -->
|
|
650
|
-
<div class="agents-empty" id="agents-empty" style="display: none;">
|
|
651
|
-
<div class="empty-icon">🤖</div>
|
|
652
|
-
<p>No agents found</p>
|
|
653
|
-
<small>Create agents in your .claude/agents directory to see them here</small>
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
|
-
|
|
657
|
-
<!-- Loading State -->
|
|
658
|
-
<div class="loading-state" id="conversations-loading" style="display: none;">
|
|
659
|
-
<div class="loading-spinner"></div>
|
|
660
|
-
<span class="loading-text">Loading conversations...</span>
|
|
661
|
-
</div>
|
|
662
|
-
|
|
663
|
-
<!-- Error State -->
|
|
664
|
-
<div class="error-state" id="conversations-error" style="display: none;">
|
|
665
|
-
<div class="error-content">
|
|
666
|
-
<span class="error-icon">⚠️</span>
|
|
667
|
-
<span class="error-message"></span>
|
|
668
|
-
<button class="error-retry" id="retry-load">Retry</button>
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
|
|
672
|
-
<!-- Console Interaction Panel (Hidden by default) -->
|
|
673
|
-
<div id="console-interaction-panel" class="console-interaction-panel" style="display: none;">
|
|
674
|
-
<div class="interaction-header">
|
|
675
|
-
<div class="interaction-title">
|
|
676
|
-
<span class="interaction-icon">⚡</span>
|
|
677
|
-
<span class="interaction-text">Claude Code needs your input</span>
|
|
678
|
-
</div>
|
|
679
|
-
<button class="interaction-close" onclick="this.hideConsoleInteraction()">×</button>
|
|
680
|
-
</div>
|
|
681
|
-
|
|
682
|
-
<div class="interaction-content">
|
|
683
|
-
<div id="interaction-description" class="interaction-description">
|
|
684
|
-
<!-- Tool description will be inserted here -->
|
|
685
|
-
</div>
|
|
686
|
-
|
|
687
|
-
<div id="interaction-prompt" class="interaction-prompt">
|
|
688
|
-
Do you want to proceed?
|
|
689
|
-
</div>
|
|
690
|
-
|
|
691
|
-
<!-- Multi-choice options -->
|
|
692
|
-
<div id="interaction-choices" class="interaction-choices" style="display: none;">
|
|
693
|
-
<!-- Radio button choices will be inserted here -->
|
|
694
|
-
</div>
|
|
695
|
-
|
|
696
|
-
<!-- Text input area -->
|
|
697
|
-
<div id="interaction-text-input" class="interaction-text-input" style="display: none;">
|
|
698
|
-
<label for="console-text-input">Your response:</label>
|
|
699
|
-
<textarea id="console-text-input" placeholder="Type your response here..." rows="4"></textarea>
|
|
700
|
-
</div>
|
|
701
|
-
|
|
702
|
-
<div class="interaction-actions">
|
|
703
|
-
<button id="interaction-submit" class="interaction-btn primary">Submit</button>
|
|
704
|
-
<button id="interaction-cancel" class="interaction-btn secondary">Cancel</button>
|
|
705
|
-
</div>
|
|
706
|
-
</div>
|
|
707
|
-
</div>
|
|
708
|
-
|
|
709
|
-
<!-- Two Column Layout -->
|
|
710
|
-
<div class="conversations-layout">
|
|
711
|
-
<!-- Left Sidebar: Conversations List -->
|
|
712
|
-
<div class="conversations-sidebar">
|
|
713
|
-
<div class="sidebar-header">
|
|
714
|
-
<h3>Chats</h3>
|
|
715
|
-
<span class="conversation-count" id="sidebar-count">0</span>
|
|
716
|
-
</div>
|
|
717
|
-
<div class="conversations-list" id="conversations-list">
|
|
718
|
-
<!-- Conversation items will be rendered here -->
|
|
719
|
-
</div>
|
|
720
|
-
|
|
721
|
-
<!-- Load More Indicator -->
|
|
722
|
-
<div class="load-more-indicator" id="load-more-indicator" style="display: none;">
|
|
723
|
-
<div class="loading-spinner"></div>
|
|
724
|
-
<span class="loading-text">Loading more conversations...</span>
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
|
|
728
|
-
<!-- Right Panel: Messages Detail -->
|
|
729
|
-
<div class="messages-panel">
|
|
730
|
-
<div class="messages-header" id="messages-header">
|
|
731
|
-
<div class="selected-conversation-info">
|
|
732
|
-
<h3 id="selected-conversation-title">Select a chat</h3>
|
|
733
|
-
<div class="selected-conversation-meta" id="selected-conversation-meta"></div>
|
|
734
|
-
</div>
|
|
735
|
-
<div class="messages-actions">
|
|
736
|
-
<button class="action-btn-small" id="export-conversation" title="Export conversation">
|
|
737
|
-
<span class="btn-icon-small">📁</span>
|
|
738
|
-
Export
|
|
739
|
-
</button>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
|
|
743
|
-
<div class="messages-content" id="messages-content">
|
|
744
|
-
<div class="no-conversation-selected">
|
|
745
|
-
<div class="no-selection-icon">💬</div>
|
|
746
|
-
<h4>No conversation selected</h4>
|
|
747
|
-
<p>Choose a conversation from the sidebar to view its messages</p>
|
|
748
|
-
</div>
|
|
749
|
-
</div>
|
|
750
|
-
|
|
751
|
-
<!-- Conversation State Banner -->
|
|
752
|
-
<div class="conversation-state-banner" id="conversation-state-banner" style="display: none;">
|
|
753
|
-
<div class="state-indicator">
|
|
754
|
-
<span class="state-dot" id="state-dot"></span>
|
|
755
|
-
<span class="state-text" id="state-text">Ready</span>
|
|
756
|
-
</div>
|
|
757
|
-
<div class="state-timestamp" id="state-timestamp"></div>
|
|
758
|
-
</div>
|
|
759
|
-
</div>
|
|
760
|
-
</div>
|
|
761
|
-
|
|
762
|
-
<!-- Empty State -->
|
|
763
|
-
<div class="empty-state" id="empty-state" style="display: none;">
|
|
764
|
-
<div class="empty-content">
|
|
765
|
-
<span class="empty-icon">💬</span>
|
|
766
|
-
<h3>No conversations found</h3>
|
|
767
|
-
<p>No agent conversations match your current filters.</p>
|
|
768
|
-
<button class="empty-action" id="clear-filters">Clear Filters</button>
|
|
769
|
-
</div>
|
|
770
|
-
</div>
|
|
771
|
-
</div>
|
|
772
|
-
`;
|
|
773
|
-
|
|
774
|
-
this.bindEvents();
|
|
775
|
-
this.setupInfiniteScroll();
|
|
776
|
-
this.initializeHeaderComponent();
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Initialize the header component
|
|
781
|
-
*/
|
|
782
|
-
initializeHeaderComponent() {
|
|
783
|
-
const headerContainer = this.container.querySelector('#agents-header-container');
|
|
784
|
-
if (headerContainer && typeof HeaderComponent !== 'undefined') {
|
|
785
|
-
this.headerComponent = new HeaderComponent(headerContainer, {
|
|
786
|
-
title: 'Claude Code Chats',
|
|
787
|
-
subtitle: 'Monitor and analyze Claude Code agent interactions in real-time',
|
|
788
|
-
version: 'v1.13.2', // Fallback version
|
|
789
|
-
showVersionBadge: true,
|
|
790
|
-
showLastUpdate: true,
|
|
791
|
-
showThemeSwitch: true,
|
|
792
|
-
showGitHubLink: true,
|
|
793
|
-
dataService: this.dataService // Pass DataService for dynamic version loading
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
this.headerComponent.render();
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Initialize child components
|
|
802
|
-
*/
|
|
803
|
-
async initializeComponents() {
|
|
804
|
-
// Initialize ConversationTable for detailed view if available
|
|
805
|
-
const tableContainer = this.container.querySelector('#conversations-table');
|
|
806
|
-
if (tableContainer && typeof ConversationTable !== 'undefined') {
|
|
807
|
-
try {
|
|
808
|
-
this.components.conversationTable = new ConversationTable(
|
|
809
|
-
tableContainer,
|
|
810
|
-
this.dataService,
|
|
811
|
-
this.stateService
|
|
812
|
-
);
|
|
813
|
-
await this.components.conversationTable.initialize();
|
|
814
|
-
} catch (error) {
|
|
815
|
-
console.warn('ConversationTable initialization failed:', error);
|
|
816
|
-
// Show fallback content
|
|
817
|
-
tableContainer.innerHTML = `
|
|
818
|
-
<div class="conversation-table-placeholder">
|
|
819
|
-
<p>Detailed table view not available</p>
|
|
820
|
-
</div>
|
|
821
|
-
`;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Bind event listeners
|
|
828
|
-
*/
|
|
829
|
-
bindEvents() {
|
|
830
|
-
// Filter controls
|
|
831
|
-
const statusFilter = this.container.querySelector('#status-filter');
|
|
832
|
-
statusFilter.addEventListener('change', (e) => this.updateFilter('status', e.target.value));
|
|
833
|
-
|
|
834
|
-
const timeFilter = this.container.querySelector('#time-filter');
|
|
835
|
-
timeFilter.addEventListener('change', (e) => this.updateFilter('timeRange', e.target.value));
|
|
836
|
-
|
|
837
|
-
const searchInput = this.container.querySelector('#search-filter');
|
|
838
|
-
searchInput.addEventListener('input', (e) => this.updateFilter('search', e.target.value));
|
|
839
|
-
|
|
840
|
-
const clearSearch = this.container.querySelector('#clear-search');
|
|
841
|
-
clearSearch.addEventListener('click', () => this.clearSearch());
|
|
842
|
-
|
|
843
|
-
// Error retry
|
|
844
|
-
const retryBtn = this.container.querySelector('#retry-load');
|
|
845
|
-
if (retryBtn) {
|
|
846
|
-
retryBtn.addEventListener('click', () => this.loadConversationsData());
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Clear filters
|
|
850
|
-
const clearFiltersBtn = this.container.querySelector('#clear-filters');
|
|
851
|
-
if (clearFiltersBtn) {
|
|
852
|
-
clearFiltersBtn.addEventListener('click', () => this.clearAllFilters());
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
// Refresh agents
|
|
857
|
-
const refreshAgentsBtn = this.container.querySelector('#refresh-agents');
|
|
858
|
-
if (refreshAgentsBtn) {
|
|
859
|
-
refreshAgentsBtn.addEventListener('click', () => this.refreshAgents());
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* Setup infinite scroll for conversations list
|
|
865
|
-
*/
|
|
866
|
-
setupInfiniteScroll() {
|
|
867
|
-
const conversationsContainer = this.container.querySelector('#conversations-list');
|
|
868
|
-
if (!conversationsContainer) return;
|
|
869
|
-
|
|
870
|
-
conversationsContainer.addEventListener('scroll', () => {
|
|
871
|
-
const { scrollTop, scrollHeight, clientHeight } = conversationsContainer;
|
|
872
|
-
const threshold = 100; // Load more when 100px from bottom
|
|
873
|
-
|
|
874
|
-
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
|
875
|
-
this.loadMoreConversations();
|
|
876
|
-
}
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Update loading indicator
|
|
882
|
-
* @param {boolean} isLoading - Whether to show loading indicator
|
|
883
|
-
*/
|
|
884
|
-
updateLoadingIndicator(isLoading) {
|
|
885
|
-
const loadingIndicator = this.container.querySelector('#load-more-indicator');
|
|
886
|
-
if (loadingIndicator) {
|
|
887
|
-
loadingIndicator.style.display = isLoading ? 'flex' : 'none';
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Load agents data from API
|
|
893
|
-
*/
|
|
894
|
-
async loadAgentsData() {
|
|
895
|
-
try {
|
|
896
|
-
this.showAgentsLoading(true);
|
|
897
|
-
|
|
898
|
-
const agentsData = await this.dataService.cachedFetch('/api/agents');
|
|
899
|
-
|
|
900
|
-
if (agentsData && agentsData.agents) {
|
|
901
|
-
this.agents = agentsData.agents;
|
|
902
|
-
this.renderAgents();
|
|
903
|
-
} else {
|
|
904
|
-
this.showAgentsEmpty();
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
} catch (error) {
|
|
908
|
-
console.error('Error loading agents data:', error);
|
|
909
|
-
this.showAgentsEmpty();
|
|
910
|
-
} finally {
|
|
911
|
-
this.showAgentsLoading(false);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Render global agents in the agents list (user-level only)
|
|
917
|
-
*/
|
|
918
|
-
renderAgents() {
|
|
919
|
-
const agentsList = this.container.querySelector('#agents-list');
|
|
920
|
-
const agentsCount = this.container.querySelector('#agents-count');
|
|
921
|
-
|
|
922
|
-
if (!agentsList || !agentsCount) return;
|
|
923
|
-
|
|
924
|
-
// Filter only global/user agents for main section
|
|
925
|
-
const globalAgents = this.agents.filter(agent => agent.level === 'user');
|
|
926
|
-
|
|
927
|
-
if (globalAgents.length === 0) {
|
|
928
|
-
this.showAgentsEmpty();
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Update count for global agents only
|
|
933
|
-
agentsCount.textContent = `${globalAgents.length} global agent${globalAgents.length !== 1 ? 's' : ''}`;
|
|
934
|
-
|
|
935
|
-
// Render global agent items (compact rectangles)
|
|
936
|
-
const agentsHTML = globalAgents.map(agent => {
|
|
937
|
-
const levelBadge = agent.level === 'project' ? 'P' : 'U';
|
|
938
|
-
|
|
939
|
-
return `
|
|
940
|
-
<div class="agent-item" data-agent-id="${agent.name}">
|
|
941
|
-
<div class="agent-dot" style="background-color: ${agent.color}"></div>
|
|
942
|
-
<span class="agent-name">${agent.name}</span>
|
|
943
|
-
<span class="agent-level-badge ${agent.level}" title="${agent.level === 'project' ? 'Project Agent' : 'User Agent'}">${levelBadge}</span>
|
|
944
|
-
</div>
|
|
945
|
-
`;
|
|
946
|
-
}).join('');
|
|
947
|
-
|
|
948
|
-
agentsList.innerHTML = agentsHTML;
|
|
949
|
-
|
|
950
|
-
// Hide empty state and show list
|
|
951
|
-
this.hideAgentsEmpty();
|
|
952
|
-
agentsList.style.display = 'block';
|
|
953
|
-
|
|
954
|
-
// Bind agent events
|
|
955
|
-
this.bindAgentEvents();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Bind events for agent items
|
|
960
|
-
*/
|
|
961
|
-
bindAgentEvents() {
|
|
962
|
-
const agentItems = this.container.querySelectorAll('.agent-item');
|
|
963
|
-
|
|
964
|
-
agentItems.forEach(item => {
|
|
965
|
-
item.addEventListener('click', () => {
|
|
966
|
-
const agentId = item.dataset.agentId;
|
|
967
|
-
this.selectAgent(agentId);
|
|
968
|
-
});
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Select an agent (opens modal with details)
|
|
974
|
-
* @param {string} agentId - Agent ID
|
|
975
|
-
*/
|
|
976
|
-
selectAgent(agentId) {
|
|
977
|
-
const agent = this.agents.find(a => a.name === agentId);
|
|
978
|
-
if (agent) {
|
|
979
|
-
this.openAgentModal(agent);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Open agent details modal
|
|
985
|
-
* @param {Object} agent - Agent object
|
|
986
|
-
*/
|
|
987
|
-
openAgentModal(agent) {
|
|
988
|
-
// If this is a specific tool, open the custom tool modal
|
|
989
|
-
if (agent.isToolDetails) {
|
|
990
|
-
this.openToolModal(agent);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
const modalHTML = `
|
|
995
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
996
|
-
<div class="agent-modal">
|
|
997
|
-
<div class="agent-modal-header">
|
|
998
|
-
<div class="agent-modal-title">
|
|
999
|
-
<div class="agent-title-main">
|
|
1000
|
-
<div class="agent-dot" style="background-color: ${agent.color}"></div>
|
|
1001
|
-
<div class="agent-title-info">
|
|
1002
|
-
<h3>${agent.name}</h3>
|
|
1003
|
-
<div class="agent-subtitle">
|
|
1004
|
-
<span class="agent-level-badge ${agent.level}">${agent.level === 'project' ? 'Project Agent' : 'User Agent'}</span>
|
|
1005
|
-
${agent.projectName ? `<span class="agent-project-name">• ${agent.projectName}</span>` : ''}
|
|
1006
|
-
</div>
|
|
1007
|
-
</div>
|
|
1008
|
-
</div>
|
|
1009
|
-
</div>
|
|
1010
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
1011
|
-
</div>
|
|
1012
|
-
|
|
1013
|
-
<div class="agent-modal-content">
|
|
1014
|
-
<div class="agent-info-section">
|
|
1015
|
-
<h4>Description</h4>
|
|
1016
|
-
<p>${agent.description}</p>
|
|
1017
|
-
</div>
|
|
1018
|
-
|
|
1019
|
-
${agent.projectName ? `
|
|
1020
|
-
<div class="agent-info-section">
|
|
1021
|
-
<h4>Project</h4>
|
|
1022
|
-
<p>${agent.projectName}</p>
|
|
1023
|
-
</div>
|
|
1024
|
-
` : ''}
|
|
1025
|
-
|
|
1026
|
-
<div class="agent-info-section">
|
|
1027
|
-
<h4>Tools Access</h4>
|
|
1028
|
-
<p>${agent.tools && agent.tools.length > 0
|
|
1029
|
-
? `Has access to: ${agent.tools.join(', ')}`
|
|
1030
|
-
: 'Has access to all available tools'}</p>
|
|
1031
|
-
</div>
|
|
1032
|
-
|
|
1033
|
-
<div class="agent-info-section">
|
|
1034
|
-
<h4>System Prompt</h4>
|
|
1035
|
-
<div class="agent-system-prompt">${agent.systemPrompt ? agent.systemPrompt.replace(/\n/g, '<br>') : 'No system prompt available'}</div>
|
|
1036
|
-
</div>
|
|
1037
|
-
|
|
1038
|
-
<div class="agent-usage-tips">
|
|
1039
|
-
<h4>💡 How to Use This Agent</h4>
|
|
1040
|
-
<div class="usage-tips-content">
|
|
1041
|
-
<p><strong>To invoke this agent explicitly:</strong></p>
|
|
1042
|
-
<code class="usage-example">Use the ${agent.name} agent to [describe your request]</code>
|
|
1043
|
-
|
|
1044
|
-
<p><strong>Alternative ways to invoke:</strong></p>
|
|
1045
|
-
<ul>
|
|
1046
|
-
<li><code>Ask the ${agent.name} agent to [task]</code></li>
|
|
1047
|
-
<li><code>Have the ${agent.name} agent [action]</code></li>
|
|
1048
|
-
<li><code>Let the ${agent.name} agent handle [request]</code></li>
|
|
1049
|
-
</ul>
|
|
1050
|
-
|
|
1051
|
-
<p><strong>Best practices:</strong></p>
|
|
1052
|
-
<ul>
|
|
1053
|
-
<li>Be specific about what you want the agent to do</li>
|
|
1054
|
-
<li>Provide context when needed</li>
|
|
1055
|
-
<li>The agent will automatically use appropriate tools</li>
|
|
1056
|
-
</ul>
|
|
1057
|
-
</div>
|
|
1058
|
-
</div>
|
|
1059
|
-
|
|
1060
|
-
<div class="agent-metadata">
|
|
1061
|
-
<small><strong>File:</strong> ${agent.filePath}</small><br>
|
|
1062
|
-
<small><strong>Last modified:</strong> ${new Date(agent.lastModified).toLocaleString()}</small>
|
|
1063
|
-
</div>
|
|
1064
|
-
</div>
|
|
1065
|
-
</div>
|
|
1066
|
-
</div>
|
|
1067
|
-
`;
|
|
1068
|
-
|
|
1069
|
-
// Add modal to DOM
|
|
1070
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1071
|
-
|
|
1072
|
-
// Bind close events
|
|
1073
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
1074
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
1075
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
1076
|
-
this.closeAgentModal();
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
// ESC key to close - store reference for cleanup
|
|
1081
|
-
this.modalKeydownHandler = (e) => {
|
|
1082
|
-
if (e.key === 'Escape') {
|
|
1083
|
-
this.closeAgentModal();
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
* Open tool-specific modal for any tool
|
|
1091
|
-
* @param {Object} toolData - Tool data object
|
|
1092
|
-
*/
|
|
1093
|
-
openToolModal(toolData) {
|
|
1094
|
-
switch (toolData.name) {
|
|
1095
|
-
case 'Read':
|
|
1096
|
-
this.openReadToolModal(toolData);
|
|
1097
|
-
break;
|
|
1098
|
-
case 'Edit':
|
|
1099
|
-
this.openEditToolModal(toolData);
|
|
1100
|
-
break;
|
|
1101
|
-
case 'Write':
|
|
1102
|
-
this.openWriteToolModal(toolData);
|
|
1103
|
-
break;
|
|
1104
|
-
case 'Bash':
|
|
1105
|
-
this.openBashToolModal(toolData);
|
|
1106
|
-
break;
|
|
1107
|
-
case 'Glob':
|
|
1108
|
-
this.openGlobToolModal(toolData);
|
|
1109
|
-
break;
|
|
1110
|
-
case 'Grep':
|
|
1111
|
-
this.openGrepToolModal(toolData);
|
|
1112
|
-
break;
|
|
1113
|
-
case 'TodoWrite':
|
|
1114
|
-
this.openTodoWriteToolModal(toolData);
|
|
1115
|
-
break;
|
|
1116
|
-
default:
|
|
1117
|
-
// Fallback to generic agent modal for unknown tools
|
|
1118
|
-
this.openAgentModal({...toolData, isToolDetails: false});
|
|
1119
|
-
break;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
/**
|
|
1124
|
-
* Open Read tool specific modal
|
|
1125
|
-
* @param {Object} readToolData - Read tool data
|
|
1126
|
-
*/
|
|
1127
|
-
openReadToolModal(readToolData) {
|
|
1128
|
-
const input = readToolData.input || {};
|
|
1129
|
-
const filePath = input.file_path || 'Unknown file';
|
|
1130
|
-
const fileName = filePath.split('/').pop() || 'Unknown';
|
|
1131
|
-
const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'txt';
|
|
1132
|
-
const offset = input.offset;
|
|
1133
|
-
const limit = input.limit;
|
|
1134
|
-
const toolId = readToolData.id || 'unknown';
|
|
1135
|
-
|
|
1136
|
-
// Analyze file context for project
|
|
1137
|
-
const isConfigFile = ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'config'].includes(fileExtension);
|
|
1138
|
-
const isDocFile = ['md', 'txt', 'rst', 'adoc'].includes(fileExtension);
|
|
1139
|
-
const isCodeFile = ['js', 'ts', 'py', 'go', 'rs', 'java', 'cpp', 'c', 'h', 'css', 'html', 'jsx', 'tsx'].includes(fileExtension);
|
|
1140
|
-
const isProjectRoot = fileName.toLowerCase().includes('claude') || fileName.toLowerCase().includes('readme') || fileName.toLowerCase().includes('package');
|
|
1141
|
-
const isTestFile = fileName.toLowerCase().includes('test') || fileName.toLowerCase().includes('spec');
|
|
1142
|
-
|
|
1143
|
-
let fileCategory = '';
|
|
1144
|
-
let filePurpose = '';
|
|
1145
|
-
let contextIcon = '📄';
|
|
1146
|
-
|
|
1147
|
-
if (isProjectRoot) {
|
|
1148
|
-
fileCategory = 'Project Documentation';
|
|
1149
|
-
filePurpose = 'Understanding project structure and setup';
|
|
1150
|
-
contextIcon = '📋';
|
|
1151
|
-
} else if (fileName.toLowerCase().includes('claude')) {
|
|
1152
|
-
fileCategory = 'Claude Configuration';
|
|
1153
|
-
filePurpose = 'Reading project instructions for AI assistant';
|
|
1154
|
-
contextIcon = '🤖';
|
|
1155
|
-
} else if (isConfigFile) {
|
|
1156
|
-
fileCategory = 'Configuration File';
|
|
1157
|
-
filePurpose = 'Understanding project settings and dependencies';
|
|
1158
|
-
contextIcon = '⚙️';
|
|
1159
|
-
} else if (isDocFile) {
|
|
1160
|
-
fileCategory = 'Documentation';
|
|
1161
|
-
filePurpose = 'Reading project documentation or specifications';
|
|
1162
|
-
contextIcon = '📚';
|
|
1163
|
-
} else if (isTestFile) {
|
|
1164
|
-
fileCategory = 'Test File';
|
|
1165
|
-
filePurpose = 'Analyzing test cases and specifications';
|
|
1166
|
-
contextIcon = '🧪';
|
|
1167
|
-
} else if (isCodeFile) {
|
|
1168
|
-
fileCategory = 'Source Code';
|
|
1169
|
-
filePurpose = 'Analyzing implementation and logic';
|
|
1170
|
-
contextIcon = '💻';
|
|
1171
|
-
} else {
|
|
1172
|
-
fileCategory = 'Project File';
|
|
1173
|
-
filePurpose = 'Reading project-related content';
|
|
1174
|
-
contextIcon = '📄';
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Get directory context
|
|
1178
|
-
const pathParts = filePath.split('/');
|
|
1179
|
-
const projectContext = pathParts.includes('claude-code-templates') ? 'Claude Code Templates Project' : 'Current Project';
|
|
1180
|
-
const relativeDir = pathParts.slice(-3, -1).join('/') || 'root';
|
|
1181
|
-
|
|
1182
|
-
const modalHTML = `
|
|
1183
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
1184
|
-
<div class="agent-modal read-tool-modal">
|
|
1185
|
-
<div class="agent-modal-header">
|
|
1186
|
-
<div class="agent-modal-title">
|
|
1187
|
-
<div class="agent-title-main">
|
|
1188
|
-
<div class="tool-icon read-tool">
|
|
1189
|
-
<span style="font-size: 20px;">${contextIcon}</span>
|
|
1190
|
-
</div>
|
|
1191
|
-
<div class="agent-title-info">
|
|
1192
|
-
<h3>File Read: ${fileName}</h3>
|
|
1193
|
-
<div class="agent-subtitle">
|
|
1194
|
-
<span class="tool-type-badge">${fileCategory}</span>
|
|
1195
|
-
<span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
|
|
1196
|
-
</div>
|
|
1197
|
-
</div>
|
|
1198
|
-
</div>
|
|
1199
|
-
</div>
|
|
1200
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
1201
|
-
</div>
|
|
1202
|
-
|
|
1203
|
-
<div class="agent-modal-content">
|
|
1204
|
-
<div class="raw-parameters-section primary-section">
|
|
1205
|
-
<h4>🔧 Tool Parameters</h4>
|
|
1206
|
-
<div class="raw-params-container">
|
|
1207
|
-
<pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
|
|
1208
|
-
</div>
|
|
1209
|
-
<div class="params-summary">
|
|
1210
|
-
<span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
|
|
1211
|
-
<span class="param-chip">File: ${fileName}</span>
|
|
1212
|
-
${offset ? `<span class="param-chip">From line: ${offset}</span>` : ''}
|
|
1213
|
-
${limit ? `<span class="param-chip">Lines: ${limit}</span>` : '<span class="param-chip">Complete file</span>'}
|
|
1214
|
-
</div>
|
|
1215
|
-
</div>
|
|
1216
|
-
|
|
1217
|
-
<div class="read-operation-section">
|
|
1218
|
-
<h4>📖 Read Operation Details</h4>
|
|
1219
|
-
<div class="operation-details">
|
|
1220
|
-
<div class="operation-item">
|
|
1221
|
-
<span class="operation-label">Full Path:</span>
|
|
1222
|
-
<code class="operation-value">${filePath}</code>
|
|
1223
|
-
</div>
|
|
1224
|
-
${offset ? `
|
|
1225
|
-
<div class="operation-item">
|
|
1226
|
-
<span class="operation-label">Starting Line:</span>
|
|
1227
|
-
<code class="operation-value">${offset}</code>
|
|
1228
|
-
</div>
|
|
1229
|
-
` : `
|
|
1230
|
-
<div class="operation-item">
|
|
1231
|
-
<span class="operation-label">Read Scope:</span>
|
|
1232
|
-
<code class="operation-value">From beginning</code>
|
|
1233
|
-
</div>
|
|
1234
|
-
`}
|
|
1235
|
-
${limit ? `
|
|
1236
|
-
<div class="operation-item">
|
|
1237
|
-
<span class="operation-label">Lines Read:</span>
|
|
1238
|
-
<code class="operation-value">${limit} lines</code>
|
|
1239
|
-
</div>
|
|
1240
|
-
` : `
|
|
1241
|
-
<div class="operation-item">
|
|
1242
|
-
<span class="operation-label">Read Scope:</span>
|
|
1243
|
-
<code class="operation-value">Complete file</code>
|
|
1244
|
-
</div>
|
|
1245
|
-
`}
|
|
1246
|
-
<div class="operation-item">
|
|
1247
|
-
<span class="operation-label">Tool ID:</span>
|
|
1248
|
-
<code class="operation-value">${toolId}</code>
|
|
1249
|
-
</div>
|
|
1250
|
-
</div>
|
|
1251
|
-
</div>
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
<div class="file-insights-section">
|
|
1255
|
-
<h4>📊 File Insights</h4>
|
|
1256
|
-
<div class="insights-grid">
|
|
1257
|
-
<div class="insight-card">
|
|
1258
|
-
<div class="insight-header">
|
|
1259
|
-
<span class="insight-icon">${contextIcon}</span>
|
|
1260
|
-
<span class="insight-title">File Classification</span>
|
|
1261
|
-
</div>
|
|
1262
|
-
<div class="insight-content">${fileCategory}</div>
|
|
1263
|
-
</div>
|
|
1264
|
-
<div class="insight-card">
|
|
1265
|
-
<div class="insight-header">
|
|
1266
|
-
<span class="insight-icon">📍</span>
|
|
1267
|
-
<span class="insight-title">Location Context</span>
|
|
1268
|
-
</div>
|
|
1269
|
-
<div class="insight-content">${relativeDir}</div>
|
|
1270
|
-
</div>
|
|
1271
|
-
<div class="insight-card">
|
|
1272
|
-
<div class="insight-header">
|
|
1273
|
-
<span class="insight-icon">🎯</span>
|
|
1274
|
-
<span class="insight-title">Read Strategy</span>
|
|
1275
|
-
</div>
|
|
1276
|
-
<div class="insight-content">${offset && limit ? `Partial read (${limit} lines from ${offset})` : limit ? `Limited read (${limit} lines)` : offset ? `From line ${offset}` : 'Complete file'}</div>
|
|
1277
|
-
</div>
|
|
1278
|
-
<div class="insight-card">
|
|
1279
|
-
<div class="insight-header">
|
|
1280
|
-
<span class="insight-icon">🚀</span>
|
|
1281
|
-
<span class="insight-title">Project Impact</span>
|
|
1282
|
-
</div>
|
|
1283
|
-
<div class="insight-content">${filePurpose}</div>
|
|
1284
|
-
</div>
|
|
1285
|
-
</div>
|
|
1286
|
-
</div>
|
|
1287
|
-
|
|
1288
|
-
</div>
|
|
1289
|
-
</div>
|
|
1290
|
-
</div>
|
|
1291
|
-
`;
|
|
1292
|
-
|
|
1293
|
-
// Add modal to DOM
|
|
1294
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1295
|
-
|
|
1296
|
-
// Bind close events
|
|
1297
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
1298
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
1299
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
1300
|
-
this.closeAgentModal();
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
// ESC key to close - store reference for cleanup
|
|
1305
|
-
this.modalKeydownHandler = (e) => {
|
|
1306
|
-
if (e.key === 'Escape') {
|
|
1307
|
-
this.closeAgentModal();
|
|
1308
|
-
}
|
|
1309
|
-
};
|
|
1310
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Open Edit tool specific modal
|
|
1315
|
-
* @param {Object} editToolData - Edit tool data
|
|
1316
|
-
*/
|
|
1317
|
-
openEditToolModal(editToolData) {
|
|
1318
|
-
const input = editToolData.input || {};
|
|
1319
|
-
const filePath = input.file_path || 'Unknown file';
|
|
1320
|
-
const fileName = filePath.split('/').pop() || 'Unknown';
|
|
1321
|
-
const oldString = input.old_string || '';
|
|
1322
|
-
const newString = input.new_string || '';
|
|
1323
|
-
const replaceAll = input.replace_all || false;
|
|
1324
|
-
const toolId = editToolData.id || 'unknown';
|
|
1325
|
-
|
|
1326
|
-
const modalHTML = `
|
|
1327
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
1328
|
-
<div class="agent-modal edit-tool-modal">
|
|
1329
|
-
<div class="agent-modal-header">
|
|
1330
|
-
<div class="agent-modal-title">
|
|
1331
|
-
<div class="agent-title-main">
|
|
1332
|
-
<div class="tool-icon edit-tool">
|
|
1333
|
-
<span style="font-size: 20px;">✏️</span>
|
|
1334
|
-
</div>
|
|
1335
|
-
<div class="agent-title-info">
|
|
1336
|
-
<h3>File Edit: ${fileName}</h3>
|
|
1337
|
-
<div class="agent-subtitle">
|
|
1338
|
-
<span class="tool-type-badge">File Modification</span>
|
|
1339
|
-
<span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
|
|
1340
|
-
</div>
|
|
1341
|
-
</div>
|
|
1342
|
-
</div>
|
|
1343
|
-
</div>
|
|
1344
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
1345
|
-
</div>
|
|
1346
|
-
|
|
1347
|
-
<div class="agent-modal-content">
|
|
1348
|
-
<div class="raw-parameters-section primary-section">
|
|
1349
|
-
<h4>🔧 Tool Parameters</h4>
|
|
1350
|
-
<div class="raw-params-container">
|
|
1351
|
-
<pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
|
|
1352
|
-
</div>
|
|
1353
|
-
<div class="params-summary">
|
|
1354
|
-
<span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
|
|
1355
|
-
<span class="param-chip">File: ${fileName}</span>
|
|
1356
|
-
<span class="param-chip">Replace All: ${replaceAll ? 'Yes' : 'No'}</span>
|
|
1357
|
-
<span class="param-chip">Change Size: ${oldString.length} → ${newString.length} chars</span>
|
|
1358
|
-
</div>
|
|
1359
|
-
</div>
|
|
1360
|
-
|
|
1361
|
-
<div class="edit-changes-section">
|
|
1362
|
-
<div class="diff-header-section">
|
|
1363
|
-
<h4>📝 Edit Diff</h4>
|
|
1364
|
-
<div class="diff-mode-toggle">
|
|
1365
|
-
<button class="diff-mode-btn active" data-mode="lines" onclick="window.switchDiffMode('lines', this)">
|
|
1366
|
-
<span>📋</span> By Lines
|
|
1367
|
-
</button>
|
|
1368
|
-
<button class="diff-mode-btn" data-mode="compact" onclick="window.switchDiffMode('compact', this)">
|
|
1369
|
-
<span>🔍</span> Smart Diff
|
|
1370
|
-
</button>
|
|
1371
|
-
</div>
|
|
1372
|
-
</div>
|
|
1373
|
-
<div class="diff-container" id="diff-container">
|
|
1374
|
-
${this.generateDiffView(oldString, newString, 'lines')}
|
|
1375
|
-
</div>
|
|
1376
|
-
</div>
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
<div class="file-insights-section">
|
|
1380
|
-
<h4>📊 Edit Insights</h4>
|
|
1381
|
-
<div class="insights-grid">
|
|
1382
|
-
<div class="insight-card">
|
|
1383
|
-
<div class="insight-header">
|
|
1384
|
-
<span class="insight-icon">📄</span>
|
|
1385
|
-
<span class="insight-title">Target File</span>
|
|
1386
|
-
</div>
|
|
1387
|
-
<div class="insight-content">${fileName}</div>
|
|
1388
|
-
</div>
|
|
1389
|
-
<div class="insight-card">
|
|
1390
|
-
<div class="insight-header">
|
|
1391
|
-
<span class="insight-icon">📏</span>
|
|
1392
|
-
<span class="insight-title">Content Change</span>
|
|
1393
|
-
</div>
|
|
1394
|
-
<div class="insight-content">${oldString.length > newString.length ? 'Reduced' : oldString.length < newString.length ? 'Expanded' : 'Same'} (${newString.length - oldString.length > 0 ? '+' : ''}${newString.length - oldString.length} chars)</div>
|
|
1395
|
-
</div>
|
|
1396
|
-
<div class="insight-card">
|
|
1397
|
-
<div class="insight-header">
|
|
1398
|
-
<span class="insight-icon">🔄</span>
|
|
1399
|
-
<span class="insight-title">Replace Mode</span>
|
|
1400
|
-
</div>
|
|
1401
|
-
<div class="insight-content">${replaceAll ? 'All Occurrences' : 'First Occurrence'}</div>
|
|
1402
|
-
</div>
|
|
1403
|
-
<div class="insight-card">
|
|
1404
|
-
<div class="insight-header">
|
|
1405
|
-
<span class="insight-icon">🎯</span>
|
|
1406
|
-
<span class="insight-title">Edit Type</span>
|
|
1407
|
-
</div>
|
|
1408
|
-
<div class="insight-content">${oldString.length === 0 ? 'Addition' : newString.length === 0 ? 'Deletion' : 'Modification'}</div>
|
|
1409
|
-
</div>
|
|
1410
|
-
</div>
|
|
1411
|
-
</div>
|
|
1412
|
-
</div>
|
|
1413
|
-
</div>
|
|
1414
|
-
</div>
|
|
1415
|
-
`;
|
|
1416
|
-
|
|
1417
|
-
// Add modal to DOM
|
|
1418
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1419
|
-
|
|
1420
|
-
// Bind close events
|
|
1421
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
1422
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
1423
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
1424
|
-
this.closeAgentModal();
|
|
1425
|
-
}
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
// ESC key to close
|
|
1429
|
-
this.modalKeydownHandler = (e) => {
|
|
1430
|
-
if (e.key === 'Escape') {
|
|
1431
|
-
this.closeAgentModal();
|
|
1432
|
-
}
|
|
1433
|
-
};
|
|
1434
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
1435
|
-
|
|
1436
|
-
// Store the tool data for mode switching
|
|
1437
|
-
window.currentEditData = { oldString, newString };
|
|
1438
|
-
|
|
1439
|
-
// Add global function for diff mode switching
|
|
1440
|
-
const self = this;
|
|
1441
|
-
window.switchDiffMode = function(mode, button) {
|
|
1442
|
-
// Update button states
|
|
1443
|
-
document.querySelectorAll('.diff-mode-btn').forEach(btn => btn.classList.remove('active'));
|
|
1444
|
-
button.classList.add('active');
|
|
1445
|
-
|
|
1446
|
-
// Update diff content
|
|
1447
|
-
const container = document.getElementById('diff-container');
|
|
1448
|
-
if (container && window.currentEditData) {
|
|
1449
|
-
container.innerHTML = self.generateDiffView(
|
|
1450
|
-
window.currentEditData.oldString,
|
|
1451
|
-
window.currentEditData.newString,
|
|
1452
|
-
mode
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
/**
|
|
1459
|
-
* Generate diff view for edit changes
|
|
1460
|
-
* @param {string} oldText - Original text
|
|
1461
|
-
* @param {string} newText - New text
|
|
1462
|
-
* @param {string} mode - 'lines' or 'compact'
|
|
1463
|
-
* @returns {string} HTML diff view
|
|
1464
|
-
*/
|
|
1465
|
-
generateDiffView(oldText, newText, mode = 'lines') {
|
|
1466
|
-
if (mode === 'compact') {
|
|
1467
|
-
return this.generateSmartDiff(oldText, newText);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Split text into lines for better diff visualization
|
|
1471
|
-
const oldLines = oldText.split('\n');
|
|
1472
|
-
const newLines = newText.split('\n');
|
|
1473
|
-
|
|
1474
|
-
// Limit lines for display (show first 20 lines max)
|
|
1475
|
-
const maxLines = 20;
|
|
1476
|
-
const oldDisplay = oldLines.slice(0, maxLines);
|
|
1477
|
-
const newDisplay = newLines.slice(0, maxLines);
|
|
1478
|
-
const oldTruncated = oldLines.length > maxLines;
|
|
1479
|
-
const newTruncated = newLines.length > maxLines;
|
|
1480
|
-
|
|
1481
|
-
let diffHtml = '<div class="diff-editor">';
|
|
1482
|
-
|
|
1483
|
-
// Header with file info
|
|
1484
|
-
diffHtml += `
|
|
1485
|
-
<div class="diff-header">
|
|
1486
|
-
<span class="diff-stats">-${oldLines.length} lines, +${newLines.length} lines</span>
|
|
1487
|
-
</div>
|
|
1488
|
-
`;
|
|
1489
|
-
|
|
1490
|
-
// Old text (removed lines)
|
|
1491
|
-
if (oldDisplay.length > 0) {
|
|
1492
|
-
diffHtml += '<div class="diff-section removed">';
|
|
1493
|
-
oldDisplay.forEach((line) => {
|
|
1494
|
-
diffHtml += `
|
|
1495
|
-
<div class="diff-line removed-line">
|
|
1496
|
-
<span class="line-prefix">-</span>
|
|
1497
|
-
<span class="line-content">${this.escapeHtml(line) || ' '}</span>
|
|
1498
|
-
</div>
|
|
1499
|
-
`;
|
|
1500
|
-
});
|
|
1501
|
-
if (oldTruncated) {
|
|
1502
|
-
diffHtml += `
|
|
1503
|
-
<div class="diff-line truncated">
|
|
1504
|
-
<span class="line-prefix"></span>
|
|
1505
|
-
<span class="line-content">... ${oldLines.length - maxLines} more lines ...</span>
|
|
1506
|
-
</div>
|
|
1507
|
-
`;
|
|
1508
|
-
}
|
|
1509
|
-
diffHtml += '</div>';
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// New text (added lines)
|
|
1513
|
-
if (newDisplay.length > 0) {
|
|
1514
|
-
diffHtml += '<div class="diff-section added">';
|
|
1515
|
-
newDisplay.forEach((line) => {
|
|
1516
|
-
diffHtml += `
|
|
1517
|
-
<div class="diff-line added-line">
|
|
1518
|
-
<span class="line-prefix">+</span>
|
|
1519
|
-
<span class="line-content">${this.escapeHtml(line) || ' '}</span>
|
|
1520
|
-
</div>
|
|
1521
|
-
`;
|
|
1522
|
-
});
|
|
1523
|
-
if (newTruncated) {
|
|
1524
|
-
diffHtml += `
|
|
1525
|
-
<div class="diff-line truncated">
|
|
1526
|
-
<span class="line-prefix"></span>
|
|
1527
|
-
<span class="line-content">... ${newLines.length - maxLines} more lines ...</span>
|
|
1528
|
-
</div>
|
|
1529
|
-
`;
|
|
1530
|
-
}
|
|
1531
|
-
diffHtml += '</div>';
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
diffHtml += '</div>';
|
|
1535
|
-
|
|
1536
|
-
return diffHtml;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
/**
|
|
1540
|
-
* Generate smart diff that shows only changed sections
|
|
1541
|
-
* @param {string} oldText - Original text
|
|
1542
|
-
* @param {string} newText - New text
|
|
1543
|
-
* @returns {string} HTML smart diff view
|
|
1544
|
-
*/
|
|
1545
|
-
generateSmartDiff(oldText, newText) {
|
|
1546
|
-
const oldLines = oldText.split('\n');
|
|
1547
|
-
const newLines = newText.split('\n');
|
|
1548
|
-
|
|
1549
|
-
// Find differences using simple line-by-line comparison
|
|
1550
|
-
const changes = this.findLineChanges(oldLines, newLines);
|
|
1551
|
-
|
|
1552
|
-
let diffHtml = '<div class="diff-editor smart-diff">';
|
|
1553
|
-
|
|
1554
|
-
// Header with file info
|
|
1555
|
-
diffHtml += `
|
|
1556
|
-
<div class="diff-header">
|
|
1557
|
-
<span class="diff-stats">Smart diff showing ${changes.length} change block${changes.length !== 1 ? 's' : ''}</span>
|
|
1558
|
-
</div>
|
|
1559
|
-
`;
|
|
1560
|
-
|
|
1561
|
-
if (changes.length === 0) {
|
|
1562
|
-
diffHtml += `
|
|
1563
|
-
<div class="no-changes">
|
|
1564
|
-
<span class="no-changes-text">No line-level changes detected (whitespace or formatting only)</span>
|
|
1565
|
-
</div>
|
|
1566
|
-
`;
|
|
1567
|
-
} else {
|
|
1568
|
-
changes.forEach((change, index) => {
|
|
1569
|
-
diffHtml += `<div class="change-block" data-change-index="${index}">`;
|
|
1570
|
-
|
|
1571
|
-
// Show removed lines
|
|
1572
|
-
if (change.removed.length > 0) {
|
|
1573
|
-
change.removed.forEach(line => {
|
|
1574
|
-
diffHtml += `
|
|
1575
|
-
<div class="diff-line removed-line smart">
|
|
1576
|
-
<span class="line-prefix">-</span>
|
|
1577
|
-
<span class="line-content">${this.escapeHtml(line) || ' '}</span>
|
|
1578
|
-
</div>
|
|
1579
|
-
`;
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// Show added lines
|
|
1584
|
-
if (change.added.length > 0) {
|
|
1585
|
-
change.added.forEach(line => {
|
|
1586
|
-
diffHtml += `
|
|
1587
|
-
<div class="diff-line added-line smart">
|
|
1588
|
-
<span class="line-prefix">+</span>
|
|
1589
|
-
<span class="line-content">${this.escapeHtml(line) || ' '}</span>
|
|
1590
|
-
</div>
|
|
1591
|
-
`;
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
diffHtml += '</div>';
|
|
1596
|
-
|
|
1597
|
-
// Add separator between change blocks (except for the last one)
|
|
1598
|
-
if (index < changes.length - 1) {
|
|
1599
|
-
diffHtml += '<div class="change-separator">⋯</div>';
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
diffHtml += '</div>';
|
|
1605
|
-
|
|
1606
|
-
return diffHtml;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
/**
|
|
1610
|
-
* Find line changes between old and new text
|
|
1611
|
-
* @param {string[]} oldLines - Original lines
|
|
1612
|
-
* @param {string[]} newLines - New lines
|
|
1613
|
-
* @returns {Array} Array of change blocks
|
|
1614
|
-
*/
|
|
1615
|
-
findLineChanges(oldLines, newLines) {
|
|
1616
|
-
const changes = [];
|
|
1617
|
-
|
|
1618
|
-
// Simple approach: if texts are different, show both
|
|
1619
|
-
// This can be enhanced with more sophisticated diff algorithms
|
|
1620
|
-
if (oldLines.join('\n') !== newLines.join('\n')) {
|
|
1621
|
-
// For now, treat as one big change block
|
|
1622
|
-
// This could be enhanced to find actual line-by-line differences
|
|
1623
|
-
changes.push({
|
|
1624
|
-
removed: oldLines.slice(0, 10), // Limit to first 10 lines
|
|
1625
|
-
added: newLines.slice(0, 10) // Limit to first 10 lines
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
return changes;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
/**
|
|
1633
|
-
* Open Write tool specific modal
|
|
1634
|
-
* @param {Object} writeToolData - Write tool data
|
|
1635
|
-
*/
|
|
1636
|
-
openWriteToolModal(writeToolData) {
|
|
1637
|
-
const input = writeToolData.input || {};
|
|
1638
|
-
const filePath = input.file_path || 'Unknown file';
|
|
1639
|
-
const fileName = filePath.split('/').pop() || 'Unknown';
|
|
1640
|
-
const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'txt';
|
|
1641
|
-
const content = input.content || '';
|
|
1642
|
-
const toolId = writeToolData.id || 'unknown';
|
|
1643
|
-
|
|
1644
|
-
// Analyze file context
|
|
1645
|
-
const isConfigFile = ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'config'].includes(fileExtension);
|
|
1646
|
-
const isDocFile = ['md', 'txt', 'rst', 'adoc'].includes(fileExtension);
|
|
1647
|
-
const isCodeFile = ['js', 'ts', 'py', 'go', 'rs', 'java', 'cpp', 'c', 'h', 'css', 'html', 'jsx', 'tsx'].includes(fileExtension);
|
|
1648
|
-
const isProjectRoot = fileName.toLowerCase().includes('claude') || fileName.toLowerCase().includes('readme') || fileName.toLowerCase().includes('package');
|
|
1649
|
-
|
|
1650
|
-
let fileCategory = '';
|
|
1651
|
-
let contextIcon = '📝';
|
|
1652
|
-
|
|
1653
|
-
if (isProjectRoot) {
|
|
1654
|
-
fileCategory = 'Project Documentation';
|
|
1655
|
-
contextIcon = '📋';
|
|
1656
|
-
} else if (fileName.toLowerCase().includes('claude')) {
|
|
1657
|
-
fileCategory = 'Claude Configuration';
|
|
1658
|
-
contextIcon = '🤖';
|
|
1659
|
-
} else if (isConfigFile) {
|
|
1660
|
-
fileCategory = 'Configuration File';
|
|
1661
|
-
contextIcon = '⚙️';
|
|
1662
|
-
} else if (isDocFile) {
|
|
1663
|
-
fileCategory = 'Documentation';
|
|
1664
|
-
contextIcon = '📚';
|
|
1665
|
-
} else if (isCodeFile) {
|
|
1666
|
-
fileCategory = 'Source Code';
|
|
1667
|
-
contextIcon = '💻';
|
|
1668
|
-
} else {
|
|
1669
|
-
fileCategory = 'Project File';
|
|
1670
|
-
contextIcon = '📝';
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
const modalHTML = `
|
|
1674
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
1675
|
-
<div class="agent-modal write-tool-modal">
|
|
1676
|
-
<div class="agent-modal-header">
|
|
1677
|
-
<div class="agent-modal-title">
|
|
1678
|
-
<div class="agent-title-main">
|
|
1679
|
-
<div class="tool-icon write-tool">
|
|
1680
|
-
<span style="font-size: 20px;">${contextIcon}</span>
|
|
1681
|
-
</div>
|
|
1682
|
-
<div class="agent-title-info">
|
|
1683
|
-
<h3>File Creation: ${fileName}</h3>
|
|
1684
|
-
<div class="agent-subtitle">
|
|
1685
|
-
<span class="tool-type-badge">${fileCategory}</span>
|
|
1686
|
-
<span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
|
|
1687
|
-
</div>
|
|
1688
|
-
</div>
|
|
1689
|
-
</div>
|
|
1690
|
-
</div>
|
|
1691
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
1692
|
-
</div>
|
|
1693
|
-
|
|
1694
|
-
<div class="agent-modal-content">
|
|
1695
|
-
<div class="raw-parameters-section primary-section">
|
|
1696
|
-
<h4>🔧 Tool Parameters</h4>
|
|
1697
|
-
<div class="raw-params-container">
|
|
1698
|
-
<pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
|
|
1699
|
-
</div>
|
|
1700
|
-
<div class="params-summary">
|
|
1701
|
-
<span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
|
|
1702
|
-
<span class="param-chip">File: ${fileName}</span>
|
|
1703
|
-
<span class="param-chip">Size: ${content.length} characters</span>
|
|
1704
|
-
<span class="param-chip">Type: ${fileExtension.toUpperCase()}</span>
|
|
1705
|
-
</div>
|
|
1706
|
-
</div>
|
|
1707
|
-
|
|
1708
|
-
<div class="file-content-section">
|
|
1709
|
-
<div class="content-header-section">
|
|
1710
|
-
<h4>📄 File Content Preview</h4>
|
|
1711
|
-
<div class="content-stats">
|
|
1712
|
-
<span class="content-stat">${content.split('\\n').length} lines</span>
|
|
1713
|
-
<span class="content-stat">${content.length} chars</span>
|
|
1714
|
-
<span class="content-stat">${Math.round(content.length / 1024 * 100) / 100} KB</span>
|
|
1715
|
-
</div>
|
|
1716
|
-
</div>
|
|
1717
|
-
<div class="content-preview-container">
|
|
1718
|
-
<pre class="content-preview">${this.escapeHtml(content.substring(0, 1000))}${content.length > 1000 ? '\\n\\n... [truncated, showing first 1000 characters]' : ''}</pre>
|
|
1719
|
-
</div>
|
|
1720
|
-
</div>
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
<div class="file-insights-section">
|
|
1724
|
-
<h4>📊 Creation Insights</h4>
|
|
1725
|
-
<div class="insights-grid">
|
|
1726
|
-
<div class="insight-card">
|
|
1727
|
-
<div class="insight-header">
|
|
1728
|
-
<span class="insight-icon">${contextIcon}</span>
|
|
1729
|
-
<span class="insight-title">File Type</span>
|
|
1730
|
-
</div>
|
|
1731
|
-
<div class="insight-content">${fileCategory}</div>
|
|
1732
|
-
</div>
|
|
1733
|
-
<div class="insight-card">
|
|
1734
|
-
<div class="insight-header">
|
|
1735
|
-
<span class="insight-icon">📏</span>
|
|
1736
|
-
<span class="insight-title">Content Size</span>
|
|
1737
|
-
</div>
|
|
1738
|
-
<div class="insight-content">${content.length} characters</div>
|
|
1739
|
-
</div>
|
|
1740
|
-
<div class="insight-card">
|
|
1741
|
-
<div class="insight-header">
|
|
1742
|
-
<span class="insight-icon">📊</span>
|
|
1743
|
-
<span class="insight-title">Structure</span>
|
|
1744
|
-
</div>
|
|
1745
|
-
<div class="insight-content">${content.split('\\n').length} lines</div>
|
|
1746
|
-
</div>
|
|
1747
|
-
<div class="insight-card">
|
|
1748
|
-
<div class="insight-header">
|
|
1749
|
-
<span class="insight-icon">🚀</span>
|
|
1750
|
-
<span class="insight-title">Purpose</span>
|
|
1751
|
-
</div>
|
|
1752
|
-
<div class="insight-content">${content.length === 0 ? 'Empty file' : content.length < 100 ? 'Simple file' : content.length < 1000 ? 'Medium file' : 'Large file'}</div>
|
|
1753
|
-
</div>
|
|
1754
|
-
</div>
|
|
1755
|
-
</div>
|
|
1756
|
-
</div>
|
|
1757
|
-
</div>
|
|
1758
|
-
</div>
|
|
1759
|
-
`;
|
|
1760
|
-
|
|
1761
|
-
// Add modal to DOM
|
|
1762
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1763
|
-
|
|
1764
|
-
// Bind close events
|
|
1765
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
1766
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
1767
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
1768
|
-
this.closeAgentModal();
|
|
1769
|
-
}
|
|
1770
|
-
});
|
|
1771
|
-
|
|
1772
|
-
// ESC key to close
|
|
1773
|
-
this.modalKeydownHandler = (e) => {
|
|
1774
|
-
if (e.key === 'Escape') {
|
|
1775
|
-
this.closeAgentModal();
|
|
1776
|
-
}
|
|
1777
|
-
};
|
|
1778
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
/**
|
|
1782
|
-
* Open Glob tool specific modal
|
|
1783
|
-
* @param {Object} globToolData - Glob tool data
|
|
1784
|
-
*/
|
|
1785
|
-
openGlobToolModal(globToolData) {
|
|
1786
|
-
console.log('🔍 Opening Glob tool modal with data:', globToolData);
|
|
1787
|
-
|
|
1788
|
-
if (!globToolData || !globToolData.input) {
|
|
1789
|
-
console.warn('⚠️ Glob tool data missing or invalid');
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
// Extract Glob parameters
|
|
1794
|
-
const pattern = globToolData.input.pattern || '';
|
|
1795
|
-
const searchPath = globToolData.input.path || '(current directory)';
|
|
1796
|
-
|
|
1797
|
-
// Categorize glob pattern
|
|
1798
|
-
let patternType = 'General';
|
|
1799
|
-
let patternDescription = 'File pattern search';
|
|
1800
|
-
|
|
1801
|
-
if (pattern.includes('**')) {
|
|
1802
|
-
patternType = 'Recursive';
|
|
1803
|
-
patternDescription = 'Deep directory search with recursive matching';
|
|
1804
|
-
} else if (pattern.includes('*')) {
|
|
1805
|
-
patternType = 'Wildcard';
|
|
1806
|
-
patternDescription = 'Basic wildcard pattern matching';
|
|
1807
|
-
} else if (pattern.includes('.')) {
|
|
1808
|
-
patternType = 'Extension';
|
|
1809
|
-
patternDescription = 'File extension filtering';
|
|
1810
|
-
} else if (pattern.includes('/')) {
|
|
1811
|
-
patternType = 'Path-based';
|
|
1812
|
-
patternDescription = 'Directory structure pattern';
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// Detect pattern specifics
|
|
1816
|
-
const isJavaScript = pattern.includes('.js') || pattern.includes('.ts') || pattern.includes('.jsx') || pattern.includes('.tsx');
|
|
1817
|
-
const isStyles = pattern.includes('.css') || pattern.includes('.scss') || pattern.includes('.sass');
|
|
1818
|
-
const isMarkdown = pattern.includes('.md') || pattern.includes('.mdx');
|
|
1819
|
-
const isConfig = pattern.includes('config') || pattern.includes('.json') || pattern.includes('.yml') || pattern.includes('.yaml');
|
|
1820
|
-
|
|
1821
|
-
let projectContext = '';
|
|
1822
|
-
if (isJavaScript) projectContext = 'JavaScript/TypeScript files';
|
|
1823
|
-
else if (isStyles) projectContext = 'Stylesheet files';
|
|
1824
|
-
else if (isMarkdown) projectContext = 'Documentation files';
|
|
1825
|
-
else if (isConfig) projectContext = 'Configuration files';
|
|
1826
|
-
else projectContext = 'General file search';
|
|
1827
|
-
|
|
1828
|
-
const modalContent = `
|
|
1829
|
-
<div class="agent-modal-header">
|
|
1830
|
-
<div class="agent-modal-title">
|
|
1831
|
-
<div class="agent-title-main">
|
|
1832
|
-
<div class="tool-icon glob-tool">
|
|
1833
|
-
<span style="font-size: 20px;">🔍</span>
|
|
1834
|
-
</div>
|
|
1835
|
-
<div class="agent-title-info">
|
|
1836
|
-
<h3>Glob Pattern: ${this.escapeHtml(pattern)}</h3>
|
|
1837
|
-
<div class="agent-subtitle">
|
|
1838
|
-
<span class="tool-type-badge">${patternType}</span>
|
|
1839
|
-
<span class="tool-id-badge">ID: ${globToolData.id ? globToolData.id.slice(-8) : 'unknown'}</span>
|
|
1840
|
-
</div>
|
|
1841
|
-
</div>
|
|
1842
|
-
</div>
|
|
1843
|
-
</div>
|
|
1844
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
1845
|
-
</div>
|
|
1846
|
-
|
|
1847
|
-
<div class="agent-modal-content">
|
|
1848
|
-
<!-- Primary Section: Tool Parameters -->
|
|
1849
|
-
<div class="raw-parameters-section primary-section">
|
|
1850
|
-
<h4>🔧 Tool Parameters</h4>
|
|
1851
|
-
<div class="raw-params-container">
|
|
1852
|
-
<pre class="raw-params-json">${this.escapeHtml(JSON.stringify(globToolData.input, null, 2))}</pre>
|
|
1853
|
-
</div>
|
|
1854
|
-
</div>
|
|
1855
|
-
|
|
1856
|
-
<!-- Pattern Analysis -->
|
|
1857
|
-
<div class="glob-pattern-section">
|
|
1858
|
-
<h4>🎯 Pattern Analysis</h4>
|
|
1859
|
-
<div class="tool-details-grid">
|
|
1860
|
-
<div class="tool-detail-item">
|
|
1861
|
-
<span class="tool-detail-label">Search Pattern:</span>
|
|
1862
|
-
<span class="tool-detail-value pattern-display">${this.escapeHtml(pattern)}</span>
|
|
1863
|
-
</div>
|
|
1864
|
-
<div class="tool-detail-item">
|
|
1865
|
-
<span class="tool-detail-label">Pattern Type:</span>
|
|
1866
|
-
<span class="tool-detail-value">${patternType}</span>
|
|
1867
|
-
</div>
|
|
1868
|
-
<div class="tool-detail-item">
|
|
1869
|
-
<span class="tool-detail-label">Description:</span>
|
|
1870
|
-
<span class="tool-detail-value">${patternDescription}</span>
|
|
1871
|
-
</div>
|
|
1872
|
-
<div class="tool-detail-item">
|
|
1873
|
-
<span class="tool-detail-label">Search Location:</span>
|
|
1874
|
-
<span class="tool-detail-value">${this.escapeHtml(searchPath)}</span>
|
|
1875
|
-
</div>
|
|
1876
|
-
<div class="tool-detail-item">
|
|
1877
|
-
<span class="tool-detail-label">Target Files:</span>
|
|
1878
|
-
<span class="tool-detail-value">${projectContext}</span>
|
|
1879
|
-
</div>
|
|
1880
|
-
</div>
|
|
1881
|
-
</div>
|
|
1882
|
-
|
|
1883
|
-
<!-- Pattern Breakdown -->
|
|
1884
|
-
<div class="glob-components-section">
|
|
1885
|
-
<h4>🧩 Pattern Components</h4>
|
|
1886
|
-
<div class="pattern-breakdown">
|
|
1887
|
-
${this.analyzeGlobPattern(pattern)}
|
|
1888
|
-
</div>
|
|
1889
|
-
</div>
|
|
1890
|
-
|
|
1891
|
-
<!-- File Discovery Insights -->
|
|
1892
|
-
<div class="glob-insights-section">
|
|
1893
|
-
<h4>📊 Discovery Insights</h4>
|
|
1894
|
-
<div class="tool-insights">
|
|
1895
|
-
<div class="insight-item">
|
|
1896
|
-
<span class="insight-label">Scope:</span>
|
|
1897
|
-
<span class="insight-value">${pattern.includes('**') ? 'Recursive (all subdirectories)' : 'Current level only'}</span>
|
|
1898
|
-
</div>
|
|
1899
|
-
<div class="insight-item">
|
|
1900
|
-
<span class="insight-label">Efficiency:</span>
|
|
1901
|
-
<span class="insight-value">${pattern.length > 20 ? 'Complex pattern (slower)' : 'Simple pattern (fast)'}</span>
|
|
1902
|
-
</div>
|
|
1903
|
-
<div class="insight-item">
|
|
1904
|
-
<span class="insight-label">Match Strategy:</span>
|
|
1905
|
-
<span class="insight-value">${pattern.includes('*') ? 'Wildcard matching' : 'Exact pattern'}</span>
|
|
1906
|
-
</div>
|
|
1907
|
-
<div class="insight-item">
|
|
1908
|
-
<span class="insight-label">File Context:</span>
|
|
1909
|
-
<span class="insight-value">${projectContext}</span>
|
|
1910
|
-
</div>
|
|
1911
|
-
</div>
|
|
1912
|
-
</div>
|
|
1913
|
-
</div>
|
|
1914
|
-
`;
|
|
1915
|
-
|
|
1916
|
-
const modalHTML = `
|
|
1917
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
1918
|
-
<div class="agent-modal glob-tool-modal">
|
|
1919
|
-
${modalContent}
|
|
1920
|
-
</div>
|
|
1921
|
-
</div>
|
|
1922
|
-
`;
|
|
1923
|
-
|
|
1924
|
-
// Add modal to DOM
|
|
1925
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1926
|
-
|
|
1927
|
-
// Bind close events
|
|
1928
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
1929
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
1930
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
1931
|
-
this.closeAgentModal();
|
|
1932
|
-
}
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
// ESC key to close - store reference for cleanup
|
|
1936
|
-
this.modalKeydownHandler = (e) => {
|
|
1937
|
-
if (e.key === 'Escape') {
|
|
1938
|
-
this.closeAgentModal();
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
/**
|
|
1945
|
-
* Analyze glob pattern components
|
|
1946
|
-
* @param {string} pattern - Glob pattern to analyze
|
|
1947
|
-
* @returns {string} HTML breakdown of pattern
|
|
1948
|
-
*/
|
|
1949
|
-
analyzeGlobPattern(pattern) {
|
|
1950
|
-
if (!pattern) return '<span class="pattern-empty">No pattern specified</span>';
|
|
1951
|
-
|
|
1952
|
-
const components = [];
|
|
1953
|
-
const parts = pattern.split('/');
|
|
1954
|
-
|
|
1955
|
-
parts.forEach((part, index) => {
|
|
1956
|
-
let description = '';
|
|
1957
|
-
let type = 'literal';
|
|
1958
|
-
|
|
1959
|
-
if (part === '**') {
|
|
1960
|
-
description = 'Recursive directory wildcard (matches any nested path)';
|
|
1961
|
-
type = 'recursive';
|
|
1962
|
-
} else if (part.includes('*')) {
|
|
1963
|
-
if (part === '*') {
|
|
1964
|
-
description = 'Match any single directory or filename';
|
|
1965
|
-
type = 'wildcard';
|
|
1966
|
-
} else if (part.startsWith('*.')) {
|
|
1967
|
-
description = `Match files with ${part.substring(2)} extension`;
|
|
1968
|
-
type = 'extension';
|
|
1969
|
-
} else {
|
|
1970
|
-
description = 'Partial wildcard match';
|
|
1971
|
-
type = 'partial-wildcard';
|
|
1972
|
-
}
|
|
1973
|
-
} else if (part.includes('?')) {
|
|
1974
|
-
description = 'Single character wildcard';
|
|
1975
|
-
type = 'char-wildcard';
|
|
1976
|
-
} else if (part.includes('[') && part.includes(']')) {
|
|
1977
|
-
description = 'Character class match';
|
|
1978
|
-
type = 'char-class';
|
|
1979
|
-
} else if (part) {
|
|
1980
|
-
description = 'Literal directory/filename';
|
|
1981
|
-
type = 'literal';
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
if (part) {
|
|
1985
|
-
components.push(`
|
|
1986
|
-
<div class="pattern-component ${type}">
|
|
1987
|
-
<code class="pattern-part">${this.escapeHtml(part)}</code>
|
|
1988
|
-
<span class="pattern-desc">${description}</span>
|
|
1989
|
-
</div>
|
|
1990
|
-
`);
|
|
1991
|
-
}
|
|
1992
|
-
});
|
|
1993
|
-
|
|
1994
|
-
return components.length > 0 ? components.join('') : '<span class="pattern-empty">Empty pattern</span>';
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
/**
|
|
1998
|
-
* Open Grep tool specific modal
|
|
1999
|
-
* @param {Object} grepToolData - Grep tool data
|
|
2000
|
-
*/
|
|
2001
|
-
openGrepToolModal(grepToolData) {
|
|
2002
|
-
console.log('🔍 Opening Grep tool modal with data:', grepToolData);
|
|
2003
|
-
|
|
2004
|
-
if (!grepToolData || !grepToolData.input) {
|
|
2005
|
-
console.warn('⚠️ Grep tool data missing or invalid');
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// Extract Grep parameters
|
|
2010
|
-
const pattern = grepToolData.input.pattern || '';
|
|
2011
|
-
const searchPath = grepToolData.input.path || '(current directory)';
|
|
2012
|
-
const outputMode = grepToolData.input.output_mode || 'files_with_matches';
|
|
2013
|
-
const glob = grepToolData.input.glob || '';
|
|
2014
|
-
const type = grepToolData.input.type || '';
|
|
2015
|
-
const caseInsensitive = grepToolData.input['-i'] || false;
|
|
2016
|
-
const contextBefore = grepToolData.input['-B'] || 0;
|
|
2017
|
-
const contextAfter = grepToolData.input['-A'] || 0;
|
|
2018
|
-
const contextAround = grepToolData.input['-C'] || 0;
|
|
2019
|
-
const showLineNumbers = grepToolData.input['-n'] || false;
|
|
2020
|
-
const multiline = grepToolData.input.multiline || false;
|
|
2021
|
-
|
|
2022
|
-
// Analyze search pattern
|
|
2023
|
-
let patternType = 'Literal';
|
|
2024
|
-
let patternComplexity = 'Simple';
|
|
2025
|
-
let searchScope = 'Text content';
|
|
2026
|
-
|
|
2027
|
-
// Detect regex patterns
|
|
2028
|
-
if (pattern.includes('.*') || pattern.includes('.+') || pattern.includes('\\w') || pattern.includes('\\d')) {
|
|
2029
|
-
patternType = 'Regular Expression';
|
|
2030
|
-
patternComplexity = 'Complex';
|
|
2031
|
-
} else if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[') || pattern.includes(']')) {
|
|
2032
|
-
patternType = 'Wildcard Pattern';
|
|
2033
|
-
patternComplexity = 'Moderate';
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// Detect specific search types
|
|
2037
|
-
if (pattern.includes('function') || pattern.includes('class') || pattern.includes('import')) {
|
|
2038
|
-
searchScope = 'Code structure';
|
|
2039
|
-
} else if (pattern.includes('TODO') || pattern.includes('FIXME') || pattern.includes('NOTE')) {
|
|
2040
|
-
searchScope = 'Code comments';
|
|
2041
|
-
} else if (pattern.includes('error') || pattern.includes('Error') || pattern.includes('exception')) {
|
|
2042
|
-
searchScope = 'Error handling';
|
|
2043
|
-
} else if (pattern.includes('test') || pattern.includes('spec') || pattern.includes('describe')) {
|
|
2044
|
-
searchScope = 'Test code';
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
// File type analysis
|
|
2048
|
-
let fileContext = 'All files';
|
|
2049
|
-
if (type) {
|
|
2050
|
-
switch (type) {
|
|
2051
|
-
case 'js': fileContext = 'JavaScript files'; break;
|
|
2052
|
-
case 'ts': fileContext = 'TypeScript files'; break;
|
|
2053
|
-
case 'py': fileContext = 'Python files'; break;
|
|
2054
|
-
case 'go': fileContext = 'Go files'; break;
|
|
2055
|
-
case 'rust': fileContext = 'Rust files'; break;
|
|
2056
|
-
case 'java': fileContext = 'Java files'; break;
|
|
2057
|
-
case 'css': fileContext = 'CSS files'; break;
|
|
2058
|
-
case 'html': fileContext = 'HTML files'; break;
|
|
2059
|
-
case 'md': fileContext = 'Markdown files'; break;
|
|
2060
|
-
default: fileContext = `${type} files`; break;
|
|
2061
|
-
}
|
|
2062
|
-
} else if (glob) {
|
|
2063
|
-
if (glob.includes('*.js') || glob.includes('*.ts')) fileContext = 'JavaScript/TypeScript files';
|
|
2064
|
-
else if (glob.includes('*.py')) fileContext = 'Python files';
|
|
2065
|
-
else if (glob.includes('*.md')) fileContext = 'Documentation files';
|
|
2066
|
-
else if (glob.includes('*.css') || glob.includes('*.scss')) fileContext = 'Stylesheet files';
|
|
2067
|
-
else fileContext = `Files matching "${glob}"`;
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
const modalContent = `
|
|
2071
|
-
<div class="agent-modal-header">
|
|
2072
|
-
<div class="agent-modal-title">
|
|
2073
|
-
<div class="agent-title-main">
|
|
2074
|
-
<div class="tool-icon grep-tool">
|
|
2075
|
-
<span style="font-size: 20px;">🔍</span>
|
|
2076
|
-
</div>
|
|
2077
|
-
<div class="agent-title-info">
|
|
2078
|
-
<h3>Grep Search: ${this.escapeHtml(pattern)}</h3>
|
|
2079
|
-
<div class="agent-subtitle">
|
|
2080
|
-
<span class="tool-type-badge">${patternType}</span>
|
|
2081
|
-
<span class="tool-id-badge">ID: ${grepToolData.id ? grepToolData.id.slice(-8) : 'unknown'}</span>
|
|
2082
|
-
</div>
|
|
2083
|
-
</div>
|
|
2084
|
-
</div>
|
|
2085
|
-
</div>
|
|
2086
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
2087
|
-
</div>
|
|
2088
|
-
|
|
2089
|
-
<div class="agent-modal-content">
|
|
2090
|
-
<!-- Primary Section: Tool Parameters -->
|
|
2091
|
-
<div class="raw-parameters-section primary-section">
|
|
2092
|
-
<h4>🔧 Tool Parameters</h4>
|
|
2093
|
-
<div class="raw-params-container">
|
|
2094
|
-
<pre class="raw-params-json">${this.escapeHtml(JSON.stringify(grepToolData.input, null, 2))}</pre>
|
|
2095
|
-
</div>
|
|
2096
|
-
</div>
|
|
2097
|
-
|
|
2098
|
-
<!-- Search Configuration -->
|
|
2099
|
-
<div class="grep-config-section">
|
|
2100
|
-
<h4>🎯 Search Configuration</h4>
|
|
2101
|
-
<div class="tool-details-grid">
|
|
2102
|
-
<div class="tool-detail-item">
|
|
2103
|
-
<span class="tool-detail-label">Search Pattern:</span>
|
|
2104
|
-
<span class="tool-detail-value search-pattern">${this.escapeHtml(pattern)}</span>
|
|
2105
|
-
</div>
|
|
2106
|
-
<div class="tool-detail-item">
|
|
2107
|
-
<span class="tool-detail-label">Pattern Type:</span>
|
|
2108
|
-
<span class="tool-detail-value">${patternType}</span>
|
|
2109
|
-
</div>
|
|
2110
|
-
<div class="tool-detail-item">
|
|
2111
|
-
<span class="tool-detail-label">Complexity:</span>
|
|
2112
|
-
<span class="tool-detail-value">${patternComplexity}</span>
|
|
2113
|
-
</div>
|
|
2114
|
-
<div class="tool-detail-item">
|
|
2115
|
-
<span class="tool-detail-label">Search Location:</span>
|
|
2116
|
-
<span class="tool-detail-value">${this.escapeHtml(searchPath)}</span>
|
|
2117
|
-
</div>
|
|
2118
|
-
<div class="tool-detail-item">
|
|
2119
|
-
<span class="tool-detail-label">Target Files:</span>
|
|
2120
|
-
<span class="tool-detail-value">${fileContext}</span>
|
|
2121
|
-
</div>
|
|
2122
|
-
<div class="tool-detail-item">
|
|
2123
|
-
<span class="tool-detail-label">Output Mode:</span>
|
|
2124
|
-
<span class="tool-detail-value">${outputMode.replace('_', ' ')}</span>
|
|
2125
|
-
</div>
|
|
2126
|
-
</div>
|
|
2127
|
-
</div>
|
|
2128
|
-
|
|
2129
|
-
<!-- Search Options -->
|
|
2130
|
-
<div class="grep-options-section">
|
|
2131
|
-
<h4>⚙️ Search Options</h4>
|
|
2132
|
-
<div class="search-options">
|
|
2133
|
-
${this.generateGrepOptionsDisplay(grepToolData.input)}
|
|
2134
|
-
</div>
|
|
2135
|
-
</div>
|
|
2136
|
-
|
|
2137
|
-
<!-- Pattern Analysis -->
|
|
2138
|
-
<div class="grep-pattern-section">
|
|
2139
|
-
<h4>🧩 Pattern Analysis</h4>
|
|
2140
|
-
<div class="pattern-analysis">
|
|
2141
|
-
${this.analyzeGrepPattern(pattern)}
|
|
2142
|
-
</div>
|
|
2143
|
-
</div>
|
|
2144
|
-
|
|
2145
|
-
<!-- Search Insights -->
|
|
2146
|
-
<div class="grep-insights-section">
|
|
2147
|
-
<h4>📊 Search Insights</h4>
|
|
2148
|
-
<div class="tool-insights">
|
|
2149
|
-
<div class="insight-item">
|
|
2150
|
-
<span class="insight-label">Search Scope:</span>
|
|
2151
|
-
<span class="insight-value">${searchScope}</span>
|
|
2152
|
-
</div>
|
|
2153
|
-
<div class="insight-item">
|
|
2154
|
-
<span class="insight-label">Case Sensitivity:</span>
|
|
2155
|
-
<span class="insight-value">${caseInsensitive ? 'Case insensitive' : 'Case sensitive'}</span>
|
|
2156
|
-
</div>
|
|
2157
|
-
<div class="insight-item">
|
|
2158
|
-
<span class="insight-label">Multiline Mode:</span>
|
|
2159
|
-
<span class="insight-value">${multiline ? 'Enabled (cross-line patterns)' : 'Disabled (single-line only)'}</span>
|
|
2160
|
-
</div>
|
|
2161
|
-
<div class="insight-item">
|
|
2162
|
-
<span class="insight-label">Context Lines:</span>
|
|
2163
|
-
<span class="insight-value">${contextAround > 0 ? `${contextAround} lines around` : contextBefore > 0 || contextAfter > 0 ? `${contextBefore} before, ${contextAfter} after` : 'None'}</span>
|
|
2164
|
-
</div>
|
|
2165
|
-
</div>
|
|
2166
|
-
</div>
|
|
2167
|
-
</div>
|
|
2168
|
-
`;
|
|
2169
|
-
|
|
2170
|
-
const modalHTML = `
|
|
2171
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
2172
|
-
<div class="agent-modal grep-tool-modal">
|
|
2173
|
-
${modalContent}
|
|
2174
|
-
</div>
|
|
2175
|
-
</div>
|
|
2176
|
-
`;
|
|
2177
|
-
|
|
2178
|
-
// Add modal to DOM
|
|
2179
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
2180
|
-
|
|
2181
|
-
// Bind close events
|
|
2182
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
2183
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
2184
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
2185
|
-
this.closeAgentModal();
|
|
2186
|
-
}
|
|
2187
|
-
});
|
|
2188
|
-
|
|
2189
|
-
// ESC key to close - store reference for cleanup
|
|
2190
|
-
this.modalKeydownHandler = (e) => {
|
|
2191
|
-
if (e.key === 'Escape') {
|
|
2192
|
-
this.closeAgentModal();
|
|
2193
|
-
}
|
|
2194
|
-
};
|
|
2195
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
/**
|
|
2199
|
-
* Generate Grep options display
|
|
2200
|
-
* @param {Object} input - Grep input parameters
|
|
2201
|
-
* @returns {string} HTML for options display
|
|
2202
|
-
*/
|
|
2203
|
-
generateGrepOptionsDisplay(input) {
|
|
2204
|
-
const options = [];
|
|
2205
|
-
|
|
2206
|
-
if (input['-i']) options.push({ name: 'Case Insensitive (-i)', status: 'enabled', desc: 'Ignore case when matching' });
|
|
2207
|
-
if (input['-n']) options.push({ name: 'Line Numbers (-n)', status: 'enabled', desc: 'Show line numbers in output' });
|
|
2208
|
-
if (input.multiline) options.push({ name: 'Multiline Mode', status: 'enabled', desc: 'Allow patterns to span multiple lines' });
|
|
2209
|
-
if (input['-A']) options.push({ name: `After Context (-A ${input['-A']})`, status: 'enabled', desc: `Show ${input['-A']} lines after matches` });
|
|
2210
|
-
if (input['-B']) options.push({ name: `Before Context (-B ${input['-B']})`, status: 'enabled', desc: `Show ${input['-B']} lines before matches` });
|
|
2211
|
-
if (input['-C']) options.push({ name: `Around Context (-C ${input['-C']})`, status: 'enabled', desc: `Show ${input['-C']} lines around matches` });
|
|
2212
|
-
if (input.glob) options.push({ name: `File Filter (--glob)`, status: 'enabled', desc: `Only search files matching "${input.glob}"` });
|
|
2213
|
-
if (input.type) options.push({ name: `File Type (--type)`, status: 'enabled', desc: `Only search ${input.type} files` });
|
|
2214
|
-
if (input.head_limit) options.push({ name: `Result Limit`, status: 'enabled', desc: `Limit to first ${input.head_limit} results` });
|
|
2215
|
-
|
|
2216
|
-
if (options.length === 0) {
|
|
2217
|
-
return '<div class="no-options">Using default search options</div>';
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
return options.map(option => `
|
|
2221
|
-
<div class="search-option ${option.status}">
|
|
2222
|
-
<div class="option-header">
|
|
2223
|
-
<span class="option-name">${option.name}</span>
|
|
2224
|
-
<span class="option-status ${option.status}">${option.status}</span>
|
|
2225
|
-
</div>
|
|
2226
|
-
<div class="option-desc">${option.desc}</div>
|
|
2227
|
-
</div>
|
|
2228
|
-
`).join('');
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* Analyze grep pattern components
|
|
2233
|
-
* @param {string} pattern - Grep pattern to analyze
|
|
2234
|
-
* @returns {string} HTML breakdown of pattern
|
|
2235
|
-
*/
|
|
2236
|
-
analyzeGrepPattern(pattern) {
|
|
2237
|
-
if (!pattern) return '<div class="pattern-empty">No pattern specified</div>';
|
|
2238
|
-
|
|
2239
|
-
const analysis = [];
|
|
2240
|
-
|
|
2241
|
-
// Detect regex components
|
|
2242
|
-
if (pattern.includes('.*')) {
|
|
2243
|
-
analysis.push({ component: '.*', desc: 'Match any characters (zero or more)', type: 'regex' });
|
|
2244
|
-
}
|
|
2245
|
-
if (pattern.includes('.+')) {
|
|
2246
|
-
analysis.push({ component: '.+', desc: 'Match any characters (one or more)', type: 'regex' });
|
|
2247
|
-
}
|
|
2248
|
-
if (pattern.includes('\\w')) {
|
|
2249
|
-
analysis.push({ component: '\\w', desc: 'Match word characters (letters, digits, underscore)', type: 'regex' });
|
|
2250
|
-
}
|
|
2251
|
-
if (pattern.includes('\\d')) {
|
|
2252
|
-
analysis.push({ component: '\\d', desc: 'Match digit characters (0-9)', type: 'regex' });
|
|
2253
|
-
}
|
|
2254
|
-
if (pattern.includes('\\s')) {
|
|
2255
|
-
analysis.push({ component: '\\s', desc: 'Match whitespace characters', type: 'regex' });
|
|
2256
|
-
}
|
|
2257
|
-
if (pattern.includes('^')) {
|
|
2258
|
-
analysis.push({ component: '^', desc: 'Match start of line', type: 'anchor' });
|
|
2259
|
-
}
|
|
2260
|
-
if (pattern.includes('$')) {
|
|
2261
|
-
analysis.push({ component: '$', desc: 'Match end of line', type: 'anchor' });
|
|
2262
|
-
}
|
|
2263
|
-
if (pattern.includes('|')) {
|
|
2264
|
-
analysis.push({ component: '|', desc: 'Logical OR (alternative patterns)', type: 'operator' });
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
// Detect character classes
|
|
2268
|
-
const charClassMatch = pattern.match(/\[([^\]]+)\]/g);
|
|
2269
|
-
if (charClassMatch) {
|
|
2270
|
-
charClassMatch.forEach(match => {
|
|
2271
|
-
analysis.push({ component: match, desc: `Match any character in the set: ${match}`, type: 'charclass' });
|
|
2272
|
-
});
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
// Detect groups
|
|
2276
|
-
const groupMatch = pattern.match(/\(([^)]+)\)/g);
|
|
2277
|
-
if (groupMatch) {
|
|
2278
|
-
groupMatch.forEach(match => {
|
|
2279
|
-
analysis.push({ component: match, desc: `Capture group: ${match}`, type: 'group' });
|
|
2280
|
-
});
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
if (analysis.length === 0) {
|
|
2284
|
-
return '<div class="pattern-simple">Simple literal text search</div>';
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
return analysis.map(item => `
|
|
2288
|
-
<div class="pattern-component ${item.type}">
|
|
2289
|
-
<code class="pattern-part">${this.escapeHtml(item.component)}</code>
|
|
2290
|
-
<span class="pattern-desc">${item.desc}</span>
|
|
2291
|
-
</div>
|
|
2292
|
-
`).join('');
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
/**
|
|
2296
|
-
* Open TodoWrite tool specific modal
|
|
2297
|
-
* @param {Object} todoToolData - TodoWrite tool data
|
|
2298
|
-
*/
|
|
2299
|
-
openTodoWriteToolModal(todoToolData) {
|
|
2300
|
-
console.log('📝 Opening TodoWrite tool modal with data:', todoToolData);
|
|
2301
|
-
|
|
2302
|
-
if (!todoToolData || !todoToolData.input) {
|
|
2303
|
-
console.warn('⚠️ TodoWrite tool data missing or invalid');
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// Extract TodoWrite parameters
|
|
2308
|
-
const todos = todoToolData.input.todos || [];
|
|
2309
|
-
const todoCount = todos.length;
|
|
2310
|
-
|
|
2311
|
-
// Analyze todos
|
|
2312
|
-
const statusCounts = {
|
|
2313
|
-
pending: 0,
|
|
2314
|
-
in_progress: 0,
|
|
2315
|
-
completed: 0
|
|
2316
|
-
};
|
|
2317
|
-
|
|
2318
|
-
const priorityCounts = {
|
|
2319
|
-
high: 0,
|
|
2320
|
-
medium: 0,
|
|
2321
|
-
low: 0
|
|
2322
|
-
};
|
|
2323
|
-
|
|
2324
|
-
todos.forEach(todo => {
|
|
2325
|
-
if (statusCounts.hasOwnProperty(todo.status)) {
|
|
2326
|
-
statusCounts[todo.status]++;
|
|
2327
|
-
}
|
|
2328
|
-
if (priorityCounts.hasOwnProperty(todo.priority)) {
|
|
2329
|
-
priorityCounts[todo.priority]++;
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// Calculate completion rate
|
|
2334
|
-
const completionRate = todoCount > 0 ? Math.round((statusCounts.completed / todoCount) * 100) : 0;
|
|
2335
|
-
|
|
2336
|
-
// Find longest and shortest todos
|
|
2337
|
-
const todoLengths = todos.map(todo => todo.content.length);
|
|
2338
|
-
const avgLength = todoLengths.length > 0 ? Math.round(todoLengths.reduce((a, b) => a + b, 0) / todoLengths.length) : 0;
|
|
2339
|
-
|
|
2340
|
-
const modalContent = `
|
|
2341
|
-
<div class="agent-modal-header">
|
|
2342
|
-
<div class="agent-modal-title">
|
|
2343
|
-
<div class="agent-title-main">
|
|
2344
|
-
<div class="tool-icon todo-tool">
|
|
2345
|
-
<span style="font-size: 20px;">📝</span>
|
|
2346
|
-
</div>
|
|
2347
|
-
<div class="agent-title-info">
|
|
2348
|
-
<h3>Todo Management: ${todoCount} tasks</h3>
|
|
2349
|
-
<div class="agent-subtitle">
|
|
2350
|
-
<span class="tool-type-badge">${completionRate}% Complete</span>
|
|
2351
|
-
<span class="tool-id-badge">ID: ${todoToolData.id ? todoToolData.id.slice(-8) : 'unknown'}</span>
|
|
2352
|
-
</div>
|
|
2353
|
-
</div>
|
|
2354
|
-
</div>
|
|
2355
|
-
</div>
|
|
2356
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
2357
|
-
</div>
|
|
2358
|
-
|
|
2359
|
-
<div class="agent-modal-content">
|
|
2360
|
-
<!-- Primary Section: Tool Parameters -->
|
|
2361
|
-
<div class="raw-parameters-section primary-section">
|
|
2362
|
-
<h4>🔧 Tool Parameters</h4>
|
|
2363
|
-
<div class="raw-params-container">
|
|
2364
|
-
<pre class="raw-params-json">${this.escapeHtml(JSON.stringify(todoToolData.input, null, 2))}</pre>
|
|
2365
|
-
</div>
|
|
2366
|
-
</div>
|
|
2367
|
-
|
|
2368
|
-
<!-- Todo Summary -->
|
|
2369
|
-
<div class="todo-summary-section">
|
|
2370
|
-
<h4>📊 Todo Summary</h4>
|
|
2371
|
-
<div class="todo-summary">
|
|
2372
|
-
<div class="summary-stats">
|
|
2373
|
-
<div class="stat-item">
|
|
2374
|
-
<span class="stat-number">${todoCount}</span>
|
|
2375
|
-
<span class="stat-label">Total Todos</span>
|
|
2376
|
-
</div>
|
|
2377
|
-
<div class="stat-item">
|
|
2378
|
-
<span class="stat-number">${completionRate}%</span>
|
|
2379
|
-
<span class="stat-label">Completion Rate</span>
|
|
2380
|
-
</div>
|
|
2381
|
-
<div class="stat-item">
|
|
2382
|
-
<span class="stat-number">${avgLength}</span>
|
|
2383
|
-
<span class="stat-label">Avg Length</span>
|
|
2384
|
-
</div>
|
|
2385
|
-
</div>
|
|
2386
|
-
</div>
|
|
2387
|
-
</div>
|
|
2388
|
-
|
|
2389
|
-
<!-- Status Breakdown -->
|
|
2390
|
-
<div class="todo-status-section">
|
|
2391
|
-
<h4>📈 Status Breakdown</h4>
|
|
2392
|
-
<div class="status-breakdown">
|
|
2393
|
-
<div class="status-item pending">
|
|
2394
|
-
<div class="status-info">
|
|
2395
|
-
<span class="status-name">Pending</span>
|
|
2396
|
-
<span class="status-count">${statusCounts.pending}</span>
|
|
2397
|
-
</div>
|
|
2398
|
-
<div class="status-bar">
|
|
2399
|
-
<div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.pending / todoCount) * 100 : 0}%"></div>
|
|
2400
|
-
</div>
|
|
2401
|
-
</div>
|
|
2402
|
-
<div class="status-item in-progress">
|
|
2403
|
-
<div class="status-info">
|
|
2404
|
-
<span class="status-name">In Progress</span>
|
|
2405
|
-
<span class="status-count">${statusCounts.in_progress}</span>
|
|
2406
|
-
</div>
|
|
2407
|
-
<div class="status-bar">
|
|
2408
|
-
<div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.in_progress / todoCount) * 100 : 0}%"></div>
|
|
2409
|
-
</div>
|
|
2410
|
-
</div>
|
|
2411
|
-
<div class="status-item completed">
|
|
2412
|
-
<div class="status-info">
|
|
2413
|
-
<span class="status-name">Completed</span>
|
|
2414
|
-
<span class="status-count">${statusCounts.completed}</span>
|
|
2415
|
-
</div>
|
|
2416
|
-
<div class="status-bar">
|
|
2417
|
-
<div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.completed / todoCount) * 100 : 0}%"></div>
|
|
2418
|
-
</div>
|
|
2419
|
-
</div>
|
|
2420
|
-
</div>
|
|
2421
|
-
</div>
|
|
2422
|
-
|
|
2423
|
-
<!-- Priority Distribution -->
|
|
2424
|
-
<div class="todo-priority-section">
|
|
2425
|
-
<h4>🎯 Priority Distribution</h4>
|
|
2426
|
-
<div class="priority-distribution">
|
|
2427
|
-
<div class="priority-item high">
|
|
2428
|
-
<span class="priority-label">High Priority</span>
|
|
2429
|
-
<span class="priority-count">${priorityCounts.high}</span>
|
|
2430
|
-
</div>
|
|
2431
|
-
<div class="priority-item medium">
|
|
2432
|
-
<span class="priority-label">Medium Priority</span>
|
|
2433
|
-
<span class="priority-count">${priorityCounts.medium}</span>
|
|
2434
|
-
</div>
|
|
2435
|
-
<div class="priority-item low">
|
|
2436
|
-
<span class="priority-label">Low Priority</span>
|
|
2437
|
-
<span class="priority-count">${priorityCounts.low}</span>
|
|
2438
|
-
</div>
|
|
2439
|
-
</div>
|
|
2440
|
-
</div>
|
|
2441
|
-
|
|
2442
|
-
<!-- Todo Items -->
|
|
2443
|
-
<div class="todo-items-section">
|
|
2444
|
-
<h4>📋 Todo Items</h4>
|
|
2445
|
-
<div class="todo-items">
|
|
2446
|
-
${this.generateTodoItemsDisplay(todos)}
|
|
2447
|
-
</div>
|
|
2448
|
-
</div>
|
|
2449
|
-
</div>
|
|
2450
|
-
`;
|
|
2451
|
-
|
|
2452
|
-
const modalHTML = `
|
|
2453
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
2454
|
-
<div class="agent-modal todo-tool-modal">
|
|
2455
|
-
${modalContent}
|
|
2456
|
-
</div>
|
|
2457
|
-
</div>
|
|
2458
|
-
`;
|
|
2459
|
-
|
|
2460
|
-
// Add modal to DOM
|
|
2461
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
2462
|
-
|
|
2463
|
-
// Bind close events
|
|
2464
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
2465
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
2466
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
2467
|
-
this.closeAgentModal();
|
|
2468
|
-
}
|
|
2469
|
-
});
|
|
2470
|
-
|
|
2471
|
-
// ESC key to close - store reference for cleanup
|
|
2472
|
-
this.modalKeydownHandler = (e) => {
|
|
2473
|
-
if (e.key === 'Escape') {
|
|
2474
|
-
this.closeAgentModal();
|
|
2475
|
-
}
|
|
2476
|
-
};
|
|
2477
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
/**
|
|
2481
|
-
* Generate todo items display
|
|
2482
|
-
* @param {Array} todos - Array of todo items
|
|
2483
|
-
* @returns {string} HTML for todo items
|
|
2484
|
-
*/
|
|
2485
|
-
generateTodoItemsDisplay(todos) {
|
|
2486
|
-
if (!todos || todos.length === 0) {
|
|
2487
|
-
return '<div class="no-todos">No todos found</div>';
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
return todos.map((todo, index) => `
|
|
2491
|
-
<div class="todo-item ${todo.status} ${todo.priority}">
|
|
2492
|
-
<div class="todo-header">
|
|
2493
|
-
<span class="todo-index">#${index + 1}</span>
|
|
2494
|
-
<span class="todo-status ${todo.status}">${todo.status.replace('_', ' ')}</span>
|
|
2495
|
-
<span class="todo-priority ${todo.priority}">${todo.priority}</span>
|
|
2496
|
-
</div>
|
|
2497
|
-
<div class="todo-content">${this.escapeHtml(todo.content)}</div>
|
|
2498
|
-
<div class="todo-meta">
|
|
2499
|
-
<span class="todo-id">ID: ${todo.id}</span>
|
|
2500
|
-
<span class="todo-length">${todo.content.length} chars</span>
|
|
2501
|
-
</div>
|
|
2502
|
-
</div>
|
|
2503
|
-
`).join('');
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
/**
|
|
2507
|
-
* Open Bash tool specific modal
|
|
2508
|
-
* @param {Object} bashToolData - Bash tool data
|
|
2509
|
-
*/
|
|
2510
|
-
openBashToolModal(bashToolData) {
|
|
2511
|
-
const input = bashToolData.input || {};
|
|
2512
|
-
const command = input.command || 'Unknown command';
|
|
2513
|
-
const description = input.description || '';
|
|
2514
|
-
const timeout = input.timeout || 120000; // Default 2 minutes
|
|
2515
|
-
const toolId = bashToolData.id || 'unknown';
|
|
2516
|
-
|
|
2517
|
-
// Analyze command type
|
|
2518
|
-
const isGitCommand = command.startsWith('git ');
|
|
2519
|
-
const isNpmCommand = command.startsWith('npm ') || command.startsWith('yarn ') || command.startsWith('pnpm ');
|
|
2520
|
-
const isFileCommand = command.includes('ls') || command.includes('cat') || command.includes('find') || command.includes('grep');
|
|
2521
|
-
const isBuildCommand = command.includes('build') || command.includes('compile') || command.includes('make');
|
|
2522
|
-
const isTestCommand = command.includes('test') || command.includes('jest') || command.includes('mocha');
|
|
2523
|
-
const isSystemCommand = command.includes('ps') || command.includes('kill') || command.includes('sudo');
|
|
2524
|
-
|
|
2525
|
-
let commandCategory = '';
|
|
2526
|
-
let contextIcon = '⚡';
|
|
2527
|
-
|
|
2528
|
-
if (isGitCommand) {
|
|
2529
|
-
commandCategory = 'Git Operation';
|
|
2530
|
-
contextIcon = '🔧';
|
|
2531
|
-
} else if (isNpmCommand) {
|
|
2532
|
-
commandCategory = 'Package Management';
|
|
2533
|
-
contextIcon = '📦';
|
|
2534
|
-
} else if (isBuildCommand) {
|
|
2535
|
-
commandCategory = 'Build Process';
|
|
2536
|
-
contextIcon = '🔨';
|
|
2537
|
-
} else if (isTestCommand) {
|
|
2538
|
-
commandCategory = 'Testing';
|
|
2539
|
-
contextIcon = '🧪';
|
|
2540
|
-
} else if (isFileCommand) {
|
|
2541
|
-
commandCategory = 'File Operations';
|
|
2542
|
-
contextIcon = '📁';
|
|
2543
|
-
} else if (isSystemCommand) {
|
|
2544
|
-
commandCategory = 'System Command';
|
|
2545
|
-
contextIcon = '🖥️';
|
|
2546
|
-
} else {
|
|
2547
|
-
commandCategory = 'Shell Command';
|
|
2548
|
-
contextIcon = '⚡';
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
// Parse command for better display
|
|
2552
|
-
const commandParts = command.split(' ');
|
|
2553
|
-
const mainCommand = commandParts[0] || '';
|
|
2554
|
-
const args = commandParts.slice(1).join(' ') || '';
|
|
2555
|
-
|
|
2556
|
-
const modalHTML = `
|
|
2557
|
-
<div class="agent-modal-overlay" id="agent-modal-overlay">
|
|
2558
|
-
<div class="agent-modal bash-tool-modal">
|
|
2559
|
-
<div class="agent-modal-header">
|
|
2560
|
-
<div class="agent-modal-title">
|
|
2561
|
-
<div class="agent-title-main">
|
|
2562
|
-
<div class="tool-icon bash-tool">
|
|
2563
|
-
<span style="font-size: 20px;">${contextIcon}</span>
|
|
2564
|
-
</div>
|
|
2565
|
-
<div class="agent-title-info">
|
|
2566
|
-
<h3>Command: ${mainCommand}</h3>
|
|
2567
|
-
<div class="agent-subtitle">
|
|
2568
|
-
<span class="tool-type-badge">${commandCategory}</span>
|
|
2569
|
-
<span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
|
|
2570
|
-
</div>
|
|
2571
|
-
</div>
|
|
2572
|
-
</div>
|
|
2573
|
-
</div>
|
|
2574
|
-
<button class="agent-modal-close" id="agent-modal-close">×</button>
|
|
2575
|
-
</div>
|
|
2576
|
-
|
|
2577
|
-
<div class="agent-modal-content">
|
|
2578
|
-
<div class="raw-parameters-section primary-section">
|
|
2579
|
-
<h4>🔧 Tool Parameters</h4>
|
|
2580
|
-
<div class="raw-params-container">
|
|
2581
|
-
<pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
|
|
2582
|
-
</div>
|
|
2583
|
-
<div class="params-summary">
|
|
2584
|
-
<span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
|
|
2585
|
-
<span class="param-chip">Command: ${mainCommand}</span>
|
|
2586
|
-
<span class="param-chip">Timeout: ${timeout/1000}s</span>
|
|
2587
|
-
${description ? `<span class="param-chip">Described: Yes</span>` : `<span class="param-chip">Described: No</span>`}
|
|
2588
|
-
</div>
|
|
2589
|
-
</div>
|
|
2590
|
-
|
|
2591
|
-
<div class="command-execution-section">
|
|
2592
|
-
<div class="command-header-section">
|
|
2593
|
-
<h4>💻 Command Execution</h4>
|
|
2594
|
-
<div class="command-stats">
|
|
2595
|
-
<span class="command-stat">${commandParts.length} parts</span>
|
|
2596
|
-
<span class="command-stat">${command.length} chars</span>
|
|
2597
|
-
<span class="command-stat">${timeout/1000}s timeout</span>
|
|
2598
|
-
</div>
|
|
2599
|
-
</div>
|
|
2600
|
-
<div class="command-display-container">
|
|
2601
|
-
<div class="command-line">
|
|
2602
|
-
<span class="command-prompt">$</span>
|
|
2603
|
-
<span class="command-text">${this.escapeHtml(command)}</span>
|
|
2604
|
-
</div>
|
|
2605
|
-
${description ? `
|
|
2606
|
-
<div class="command-description">
|
|
2607
|
-
<span class="description-label">Description:</span>
|
|
2608
|
-
<span class="description-text">${this.escapeHtml(description)}</span>
|
|
2609
|
-
</div>
|
|
2610
|
-
` : ''}
|
|
2611
|
-
</div>
|
|
2612
|
-
</div>
|
|
2613
|
-
|
|
2614
|
-
<div class="command-analysis-section">
|
|
2615
|
-
<h4>🔍 Command Analysis</h4>
|
|
2616
|
-
<div class="analysis-grid">
|
|
2617
|
-
<div class="analysis-item">
|
|
2618
|
-
<span class="analysis-label">Main Command:</span>
|
|
2619
|
-
<code class="analysis-value">${this.escapeHtml(mainCommand)}</code>
|
|
2620
|
-
</div>
|
|
2621
|
-
${args ? `
|
|
2622
|
-
<div class="analysis-item">
|
|
2623
|
-
<span class="analysis-label">Arguments:</span>
|
|
2624
|
-
<code class="analysis-value">${this.escapeHtml(args)}</code>
|
|
2625
|
-
</div>
|
|
2626
|
-
` : ''}
|
|
2627
|
-
<div class="analysis-item">
|
|
2628
|
-
<span class="analysis-label">Category:</span>
|
|
2629
|
-
<code class="analysis-value">${commandCategory}</code>
|
|
2630
|
-
</div>
|
|
2631
|
-
<div class="analysis-item">
|
|
2632
|
-
<span class="analysis-label">Timeout:</span>
|
|
2633
|
-
<code class="analysis-value">${timeout === 120000 ? 'Default (2min)' : `${timeout/1000}s`}</code>
|
|
2634
|
-
</div>
|
|
2635
|
-
</div>
|
|
2636
|
-
</div>
|
|
2637
|
-
|
|
2638
|
-
<div class="file-insights-section">
|
|
2639
|
-
<h4>📊 Execution Insights</h4>
|
|
2640
|
-
<div class="insights-grid">
|
|
2641
|
-
<div class="insight-card">
|
|
2642
|
-
<div class="insight-header">
|
|
2643
|
-
<span class="insight-icon">${contextIcon}</span>
|
|
2644
|
-
<span class="insight-title">Command Type</span>
|
|
2645
|
-
</div>
|
|
2646
|
-
<div class="insight-content">${commandCategory}</div>
|
|
2647
|
-
</div>
|
|
2648
|
-
<div class="insight-card">
|
|
2649
|
-
<div class="insight-header">
|
|
2650
|
-
<span class="insight-icon">⏱️</span>
|
|
2651
|
-
<span class="insight-title">Timeout</span>
|
|
2652
|
-
</div>
|
|
2653
|
-
<div class="insight-content">${timeout/1000} seconds</div>
|
|
2654
|
-
</div>
|
|
2655
|
-
<div class="insight-card">
|
|
2656
|
-
<div class="insight-header">
|
|
2657
|
-
<span class="insight-icon">📝</span>
|
|
2658
|
-
<span class="insight-title">Complexity</span>
|
|
2659
|
-
</div>
|
|
2660
|
-
<div class="insight-content">${commandParts.length < 3 ? 'Simple' : commandParts.length < 6 ? 'Medium' : 'Complex'}</div>
|
|
2661
|
-
</div>
|
|
2662
|
-
<div class="insight-card">
|
|
2663
|
-
<div class="insight-header">
|
|
2664
|
-
<span class="insight-icon">🎯</span>
|
|
2665
|
-
<span class="insight-title">Documentation</span>
|
|
2666
|
-
</div>
|
|
2667
|
-
<div class="insight-content">${description ? 'Documented' : 'No description'}</div>
|
|
2668
|
-
</div>
|
|
2669
|
-
</div>
|
|
2670
|
-
</div>
|
|
2671
|
-
</div>
|
|
2672
|
-
</div>
|
|
2673
|
-
</div>
|
|
2674
|
-
`;
|
|
2675
|
-
|
|
2676
|
-
// Add modal to DOM
|
|
2677
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
2678
|
-
|
|
2679
|
-
// Bind close events
|
|
2680
|
-
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
|
|
2681
|
-
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
|
|
2682
|
-
if (e.target.id === 'agent-modal-overlay') {
|
|
2683
|
-
this.closeAgentModal();
|
|
2684
|
-
}
|
|
2685
|
-
});
|
|
2686
|
-
|
|
2687
|
-
// ESC key to close
|
|
2688
|
-
this.modalKeydownHandler = (e) => {
|
|
2689
|
-
if (e.key === 'Escape') {
|
|
2690
|
-
this.closeAgentModal();
|
|
2691
|
-
}
|
|
2692
|
-
};
|
|
2693
|
-
document.addEventListener('keydown', this.modalKeydownHandler);
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
/**
|
|
2697
|
-
* Escape HTML to prevent XSS
|
|
2698
|
-
* @param {string} text - Text to escape
|
|
2699
|
-
* @returns {string} Escaped text
|
|
2700
|
-
*/
|
|
2701
|
-
escapeHtml(text) {
|
|
2702
|
-
if (typeof text !== 'string') return String(text);
|
|
2703
|
-
|
|
2704
|
-
const div = document.createElement('div');
|
|
2705
|
-
div.textContent = text;
|
|
2706
|
-
return div.innerHTML;
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
/**
|
|
2710
|
-
* Close agent modal
|
|
2711
|
-
*/
|
|
2712
|
-
closeAgentModal() {
|
|
2713
|
-
const modal = document.getElementById('agent-modal-overlay');
|
|
2714
|
-
if (modal) {
|
|
2715
|
-
modal.remove();
|
|
2716
|
-
}
|
|
2717
|
-
if (this.modalKeydownHandler) {
|
|
2718
|
-
document.removeEventListener('keydown', this.modalKeydownHandler);
|
|
2719
|
-
this.modalKeydownHandler = null;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
/**
|
|
2724
|
-
* Show agents loading state
|
|
2725
|
-
* @param {boolean} show - Whether to show loading
|
|
2726
|
-
*/
|
|
2727
|
-
showAgentsLoading(show) {
|
|
2728
|
-
const loading = this.container.querySelector('#agents-loading');
|
|
2729
|
-
const list = this.container.querySelector('#agents-list');
|
|
2730
|
-
|
|
2731
|
-
if (loading && list) {
|
|
2732
|
-
loading.style.display = show ? 'flex' : 'none';
|
|
2733
|
-
list.style.display = show ? 'none' : 'block';
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
/**
|
|
2738
|
-
* Show agents empty state
|
|
2739
|
-
*/
|
|
2740
|
-
showAgentsEmpty() {
|
|
2741
|
-
const empty = this.container.querySelector('#agents-empty');
|
|
2742
|
-
const list = this.container.querySelector('#agents-list');
|
|
2743
|
-
const count = this.container.querySelector('#agents-count');
|
|
2744
|
-
|
|
2745
|
-
if (empty && list && count) {
|
|
2746
|
-
empty.style.display = 'flex';
|
|
2747
|
-
list.style.display = 'none';
|
|
2748
|
-
count.textContent = '0 agents';
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
/**
|
|
2753
|
-
* Hide agents empty state
|
|
2754
|
-
*/
|
|
2755
|
-
hideAgentsEmpty() {
|
|
2756
|
-
const empty = this.container.querySelector('#agents-empty');
|
|
2757
|
-
if (empty) {
|
|
2758
|
-
empty.style.display = 'none';
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
/**
|
|
2763
|
-
* Get project-specific agents for a conversation
|
|
2764
|
-
* @param {string} conversationId - Conversation ID
|
|
2765
|
-
* @returns {Array} Array of project agents for this conversation
|
|
2766
|
-
*/
|
|
2767
|
-
getAgentsForConversation(conversationId) {
|
|
2768
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2769
|
-
const conversation = conversations.find(conv => conv.id === conversationId);
|
|
2770
|
-
|
|
2771
|
-
if (!conversation || !conversation.project) {
|
|
2772
|
-
// Return empty array if no project (global agents are shown in main section)
|
|
2773
|
-
return [];
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
const projectName = conversation.project;
|
|
2777
|
-
|
|
2778
|
-
// Return only project agents for this specific project
|
|
2779
|
-
return this.agents.filter(agent =>
|
|
2780
|
-
agent.level === 'project' && agent.projectName === projectName
|
|
2781
|
-
);
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
/**
|
|
2785
|
-
* Show project agents modal
|
|
2786
|
-
* @param {string} conversationId - Conversation ID
|
|
2787
|
-
*/
|
|
2788
|
-
showProjectAgents(conversationId) {
|
|
2789
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
2790
|
-
const conversation = conversations.find(conv => conv.id === conversationId);
|
|
2791
|
-
const projectAgents = this.getAgentsForConversation(conversationId);
|
|
2792
|
-
|
|
2793
|
-
const projectName = conversation?.project || 'Unknown Project';
|
|
2794
|
-
const chatTitle = conversation?.title || `Chat ${conversationId.slice(-8)}`;
|
|
2795
|
-
|
|
2796
|
-
const modalHTML = `
|
|
2797
|
-
<div class="agent-modal-overlay" id="project-agents-modal-overlay">
|
|
2798
|
-
<div class="agent-modal project-agents-modal">
|
|
2799
|
-
<div class="agent-modal-header">
|
|
2800
|
-
<div class="agent-modal-title">
|
|
2801
|
-
<div class="agent-title-main">
|
|
2802
|
-
<div class="project-icon">📁</div>
|
|
2803
|
-
<div class="agent-title-info">
|
|
2804
|
-
<h3>Project Agents</h3>
|
|
2805
|
-
<div class="agent-subtitle">
|
|
2806
|
-
<span class="project-info">${chatTitle}</span>
|
|
2807
|
-
<span class="agent-project-name">• ${projectName}</span>
|
|
2808
|
-
</div>
|
|
2809
|
-
</div>
|
|
2810
|
-
</div>
|
|
2811
|
-
</div>
|
|
2812
|
-
<button class="agent-modal-close" id="project-agents-modal-close">×</button>
|
|
2813
|
-
</div>
|
|
2814
|
-
|
|
2815
|
-
<div class="agent-modal-content">
|
|
2816
|
-
${projectAgents.length === 0 ? `
|
|
2817
|
-
<div class="no-agents-message">
|
|
2818
|
-
<div class="no-agents-icon">🤖</div>
|
|
2819
|
-
<h4>No project agents</h4>
|
|
2820
|
-
<p>This project doesn't have any specific agents configured.</p>
|
|
2821
|
-
<p>Create agents in your project's <code>.claude/agents/</code> directory to see them here.</p>
|
|
2822
|
-
<p><strong>Note:</strong> Global agents are available in the main agents section.</p>
|
|
2823
|
-
</div>
|
|
2824
|
-
` : `
|
|
2825
|
-
<div class="project-agents-grid">
|
|
2826
|
-
${projectAgents.map(agent => `
|
|
2827
|
-
<div class="project-agent-card" data-agent-id="${agent.name}">
|
|
2828
|
-
<div class="project-agent-header">
|
|
2829
|
-
<div class="agent-dot" style="background-color: ${agent.color}"></div>
|
|
2830
|
-
<div class="project-agent-info">
|
|
2831
|
-
<h4>${agent.name}</h4>
|
|
2832
|
-
<span class="agent-level-badge ${agent.level}">${agent.level === 'project' ? 'Project' : 'User'}</span>
|
|
2833
|
-
</div>
|
|
2834
|
-
</div>
|
|
2835
|
-
<div class="project-agent-description">
|
|
2836
|
-
${this.truncateText(agent.description, 100)}
|
|
2837
|
-
</div>
|
|
2838
|
-
<div class="project-agent-footer">
|
|
2839
|
-
<span class="project-agent-tools">${agent.tools && agent.tools.length > 0 ? `${agent.tools.length} tools` : 'All tools'}</span>
|
|
2840
|
-
<button class="project-agent-details-btn" data-agent-id="${agent.name}">Details</button>
|
|
2841
|
-
</div>
|
|
2842
|
-
</div>
|
|
2843
|
-
`).join('')}
|
|
2844
|
-
</div>
|
|
2845
|
-
|
|
2846
|
-
<div class="usage-instruction">
|
|
2847
|
-
<h4>💡 How to use these agents</h4>
|
|
2848
|
-
<p>In your conversation, mention any agent by name:</p>
|
|
2849
|
-
<div class="usage-examples">
|
|
2850
|
-
${projectAgents.slice(0, 3).map(agent =>
|
|
2851
|
-
`<code class="usage-example">Use the ${agent.name} agent to help with this task</code>`
|
|
2852
|
-
).join('')}
|
|
2853
|
-
</div>
|
|
2854
|
-
</div>
|
|
2855
|
-
`}
|
|
2856
|
-
</div>
|
|
2857
|
-
</div>
|
|
2858
|
-
</div>
|
|
2859
|
-
`;
|
|
2860
|
-
|
|
2861
|
-
// Add modal to DOM
|
|
2862
|
-
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
2863
|
-
|
|
2864
|
-
// Bind close events
|
|
2865
|
-
document.getElementById('project-agents-modal-close').addEventListener('click', () => this.closeProjectAgentsModal());
|
|
2866
|
-
document.getElementById('project-agents-modal-overlay').addEventListener('click', (e) => {
|
|
2867
|
-
if (e.target.id === 'project-agents-modal-overlay') {
|
|
2868
|
-
this.closeProjectAgentsModal();
|
|
2869
|
-
}
|
|
2870
|
-
});
|
|
2871
|
-
|
|
2872
|
-
// Bind agent detail buttons
|
|
2873
|
-
const detailButtons = document.querySelectorAll('.project-agent-details-btn');
|
|
2874
|
-
detailButtons.forEach(btn => {
|
|
2875
|
-
btn.addEventListener('click', (e) => {
|
|
2876
|
-
e.stopPropagation();
|
|
2877
|
-
const agentId = btn.dataset.agentId;
|
|
2878
|
-
const agent = this.agents.find(a => a.name === agentId);
|
|
2879
|
-
if (agent) {
|
|
2880
|
-
this.closeProjectAgentsModal();
|
|
2881
|
-
this.openAgentModal(agent);
|
|
2882
|
-
}
|
|
2883
|
-
});
|
|
2884
|
-
});
|
|
2885
|
-
|
|
2886
|
-
// ESC key to close
|
|
2887
|
-
this.projectModalKeydownHandler = (e) => {
|
|
2888
|
-
if (e.key === 'Escape') {
|
|
2889
|
-
this.closeProjectAgentsModal();
|
|
2890
|
-
}
|
|
2891
|
-
};
|
|
2892
|
-
document.addEventListener('keydown', this.projectModalKeydownHandler);
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
/**
|
|
2896
|
-
* Close project agents modal
|
|
2897
|
-
*/
|
|
2898
|
-
closeProjectAgentsModal() {
|
|
2899
|
-
const modal = document.getElementById('project-agents-modal-overlay');
|
|
2900
|
-
if (modal) {
|
|
2901
|
-
modal.remove();
|
|
2902
|
-
}
|
|
2903
|
-
if (this.projectModalKeydownHandler) {
|
|
2904
|
-
document.removeEventListener('keydown', this.projectModalKeydownHandler);
|
|
2905
|
-
this.projectModalKeydownHandler = null;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
|
|
2909
|
-
/**
|
|
2910
|
-
* Refresh agents data
|
|
2911
|
-
*/
|
|
2912
|
-
async refreshAgents() {
|
|
2913
|
-
const refreshBtn = this.container.querySelector('#refresh-agents');
|
|
2914
|
-
if (refreshBtn) {
|
|
2915
|
-
refreshBtn.disabled = true;
|
|
2916
|
-
const iconElement = refreshBtn.querySelector('.btn-icon');
|
|
2917
|
-
if (iconElement) {
|
|
2918
|
-
iconElement.style.animation = 'spin 1s linear infinite';
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
try {
|
|
2923
|
-
// Just reload agents data without clearing cache
|
|
2924
|
-
await this.loadAgentsData();
|
|
2925
|
-
} catch (error) {
|
|
2926
|
-
console.error('Error refreshing agents:', error);
|
|
2927
|
-
} finally {
|
|
2928
|
-
if (refreshBtn) {
|
|
2929
|
-
refreshBtn.disabled = false;
|
|
2930
|
-
const iconElement = refreshBtn.querySelector('.btn-icon');
|
|
2931
|
-
if (iconElement) {
|
|
2932
|
-
iconElement.style.animation = '';
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
/**
|
|
2939
|
-
* Load initial conversations data using paginated API
|
|
2940
|
-
*/
|
|
2941
|
-
async loadConversationsData() {
|
|
2942
|
-
try {
|
|
2943
|
-
|
|
2944
|
-
// Reset pagination state
|
|
2945
|
-
this.pagination = {
|
|
2946
|
-
currentPage: 0,
|
|
2947
|
-
limit: 10,
|
|
2948
|
-
hasMore: true,
|
|
2949
|
-
isLoading: false
|
|
2950
|
-
};
|
|
2951
|
-
this.loadedConversations = [];
|
|
2952
|
-
this.loadedMessages.clear(); // Clear message cache too
|
|
2953
|
-
|
|
2954
|
-
// Clear the list container
|
|
2955
|
-
const listContainer = this.container.querySelector('#conversations-list');
|
|
2956
|
-
if (listContainer) {
|
|
2957
|
-
listContainer.innerHTML = '';
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
// Hide empty state initially
|
|
2961
|
-
this.hideEmptyState();
|
|
2962
|
-
|
|
2963
|
-
// Load first page and states
|
|
2964
|
-
await this.loadMoreConversations();
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
} catch (error) {
|
|
2968
|
-
console.error('Error loading conversations data:', error);
|
|
2969
|
-
this.stateService.setError('Failed to load conversations data');
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
/**
|
|
2974
|
-
* Load more conversations (pagination)
|
|
2975
|
-
*/
|
|
2976
|
-
async loadMoreConversations() {
|
|
2977
|
-
if (this.pagination.isLoading || !this.pagination.hasMore) {
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
try {
|
|
2982
|
-
|
|
2983
|
-
this.pagination.isLoading = true;
|
|
2984
|
-
this.updateLoadingIndicator(true);
|
|
2985
|
-
|
|
2986
|
-
const [conversationsData, statesData] = await Promise.all([
|
|
2987
|
-
this.dataService.getConversationsPaginated(this.pagination.currentPage, this.pagination.limit),
|
|
2988
|
-
this.dataService.getConversationStates()
|
|
2989
|
-
]);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
// Update pagination info
|
|
2993
|
-
this.pagination.hasMore = conversationsData.pagination.hasMore;
|
|
2994
|
-
this.pagination.currentPage = conversationsData.pagination.page + 1;
|
|
2995
|
-
this.pagination.totalCount = conversationsData.pagination.totalCount;
|
|
2996
|
-
|
|
2997
|
-
// Get only NEW conversations for this page
|
|
2998
|
-
const newConversations = conversationsData.conversations;
|
|
2999
|
-
|
|
3000
|
-
// Add new conversations to loaded list
|
|
3001
|
-
this.loadedConversations.push(...newConversations);
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
// Extract activeStates from the response structure
|
|
3005
|
-
const activeStates = statesData?.activeStates || {};
|
|
3006
|
-
|
|
3007
|
-
// Update state with correct format
|
|
3008
|
-
this.stateService.updateConversations(this.loadedConversations);
|
|
3009
|
-
this.stateService.updateConversationStates(activeStates);
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
// For initial load (page 0), replace content. For subsequent loads, append
|
|
3013
|
-
const isInitialLoad = conversationsData.pagination.page === 0;
|
|
3014
|
-
this.renderConversationsList(
|
|
3015
|
-
isInitialLoad ? this.loadedConversations : newConversations,
|
|
3016
|
-
activeStates,
|
|
3017
|
-
!isInitialLoad
|
|
3018
|
-
);
|
|
3019
|
-
|
|
3020
|
-
} catch (error) {
|
|
3021
|
-
console.error('Error loading more conversations:', error);
|
|
3022
|
-
this.stateService.setError('Failed to load more conversations');
|
|
3023
|
-
} finally {
|
|
3024
|
-
this.pagination.isLoading = false;
|
|
3025
|
-
this.updateLoadingIndicator(false);
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
/**
|
|
3030
|
-
* Render conversations list
|
|
3031
|
-
* @param {Array} conversations - Conversations data
|
|
3032
|
-
* @param {Object} states - Conversation states
|
|
3033
|
-
* @param {boolean} append - Whether to append or replace content
|
|
3034
|
-
*/
|
|
3035
|
-
renderConversationsList(conversations, states, append = false) {
|
|
3036
|
-
const listContainer = this.container.querySelector('#conversations-list');
|
|
3037
|
-
if (!listContainer) {
|
|
3038
|
-
console.warn('conversations-list element not found in AgentsPage - component may not be active');
|
|
3039
|
-
return;
|
|
3040
|
-
}
|
|
3041
|
-
const filteredConversations = this.filterConversations(conversations, states);
|
|
3042
|
-
|
|
3043
|
-
// Calculate count based on filters
|
|
3044
|
-
let countToShow;
|
|
3045
|
-
const hasActiveFilters = this.hasActiveFilters();
|
|
3046
|
-
|
|
3047
|
-
if (!hasActiveFilters && this.pagination && this.pagination.totalCount) {
|
|
3048
|
-
// No filters active, show total count from server
|
|
3049
|
-
countToShow = this.pagination.totalCount;
|
|
3050
|
-
} else {
|
|
3051
|
-
// Filters active, count filtered loaded conversations
|
|
3052
|
-
const conversationsToCount = this.loadedConversations && this.loadedConversations.length > 0
|
|
3053
|
-
? this.loadedConversations
|
|
3054
|
-
: conversations;
|
|
3055
|
-
const allFilteredConversations = this.filterConversations(conversationsToCount, states);
|
|
3056
|
-
countToShow = allFilteredConversations.length;
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
this.updateResultsCount(countToShow, hasActiveFilters);
|
|
3060
|
-
this.updateClearFiltersButton();
|
|
3061
|
-
|
|
3062
|
-
if (filteredConversations.length === 0 && !append) {
|
|
3063
|
-
this.showEmptyState();
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
this.hideEmptyState();
|
|
3068
|
-
|
|
3069
|
-
const conversationHTML = filteredConversations.map(conv => {
|
|
3070
|
-
const state = states[conv.id] || 'unknown';
|
|
3071
|
-
const stateClass = this.getStateClass(state);
|
|
3072
|
-
|
|
3073
|
-
// Check for agent usage
|
|
3074
|
-
const agentColor = this.getAgentColorForConversation(conv.id);
|
|
3075
|
-
const agentName = this.getAgentNameForConversation(conv.id);
|
|
3076
|
-
|
|
3077
|
-
// Generate title with agent indicator
|
|
3078
|
-
const titleColor = agentColor ? `style="color: ${agentColor}; border-left: 3px solid ${agentColor}; padding-left: 8px;"` : '';
|
|
3079
|
-
const agentIndicator = agentName ? `<span class="agent-indicator-small" style="background-color: ${agentColor}" title="Using ${agentName} agent">🤖</span>` : '';
|
|
3080
|
-
|
|
3081
|
-
return `
|
|
3082
|
-
<div class="sidebar-conversation-item" data-id="${conv.id}" ${agentColor ? `data-agent-color="${agentColor}"` : ''}>
|
|
3083
|
-
<div class="sidebar-conversation-header">
|
|
3084
|
-
<div class="sidebar-conversation-title" ${titleColor}>
|
|
3085
|
-
<span class="status-dot ${stateClass}"></span>
|
|
3086
|
-
<h4 class="sidebar-conversation-name">${conv.title || `Chat ${conv.id.slice(-8)}`}</h4>
|
|
3087
|
-
${agentIndicator}
|
|
3088
|
-
</div>
|
|
3089
|
-
<span class="sidebar-conversation-badge ${stateClass}">${this.getStateLabel(state)}</span>
|
|
3090
|
-
</div>
|
|
3091
|
-
|
|
3092
|
-
<div class="sidebar-conversation-meta">
|
|
3093
|
-
<span class="sidebar-meta-item">
|
|
3094
|
-
<span class="sidebar-meta-icon">📁</span>
|
|
3095
|
-
${this.truncateText(conv.project || 'Unknown', 12)}
|
|
3096
|
-
</span>
|
|
3097
|
-
</div>
|
|
3098
|
-
|
|
3099
|
-
<div class="sidebar-conversation-preview">
|
|
3100
|
-
<p class="sidebar-preview-text">${this.getSimpleConversationPreview(conv)}</p>
|
|
3101
|
-
</div>
|
|
3102
|
-
|
|
3103
|
-
<div class="sidebar-conversation-actions">
|
|
3104
|
-
<button class="conversation-agents-btn" data-conversation-id="${conv.id}" title="View available agents for this project">
|
|
3105
|
-
<span class="agents-icon">🤖</span>
|
|
3106
|
-
<span class="agents-text">Agents</span>
|
|
3107
|
-
</button>
|
|
3108
|
-
</div>
|
|
3109
|
-
</div>
|
|
3110
|
-
`;
|
|
3111
|
-
}).join('');
|
|
3112
|
-
|
|
3113
|
-
if (append) {
|
|
3114
|
-
listContainer.insertAdjacentHTML('beforeend', conversationHTML);
|
|
3115
|
-
} else {
|
|
3116
|
-
listContainer.innerHTML = conversationHTML;
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
// Bind card actions
|
|
3120
|
-
this.bindListActions();
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
/**
|
|
3124
|
-
* Bind list action events
|
|
3125
|
-
*/
|
|
3126
|
-
bindListActions() {
|
|
3127
|
-
// Export conversation button
|
|
3128
|
-
const exportBtn = this.container.querySelector('#export-conversation');
|
|
3129
|
-
if (exportBtn) {
|
|
3130
|
-
exportBtn.addEventListener('click', () => {
|
|
3131
|
-
if (this.selectedConversationId) {
|
|
3132
|
-
this.exportSingleConversation(this.selectedConversationId);
|
|
3133
|
-
}
|
|
3134
|
-
});
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
// Click on sidebar conversation item to select and view
|
|
3138
|
-
const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
|
|
3139
|
-
conversationItems.forEach(item => {
|
|
3140
|
-
item.addEventListener('click', (e) => {
|
|
3141
|
-
// Don't select conversation if clicking on agents button
|
|
3142
|
-
if (e.target.closest('.conversation-agents-btn')) {
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
const conversationId = item.dataset.id;
|
|
3146
|
-
this.selectConversation(conversationId);
|
|
3147
|
-
});
|
|
3148
|
-
});
|
|
3149
|
-
|
|
3150
|
-
// Bind agents button clicks
|
|
3151
|
-
const agentsButtons = this.container.querySelectorAll('.conversation-agents-btn');
|
|
3152
|
-
agentsButtons.forEach(btn => {
|
|
3153
|
-
btn.addEventListener('click', (e) => {
|
|
3154
|
-
e.stopPropagation();
|
|
3155
|
-
const conversationId = btn.dataset.conversationId;
|
|
3156
|
-
this.showProjectAgents(conversationId);
|
|
3157
|
-
});
|
|
3158
|
-
});
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
/**
|
|
3162
|
-
* Select and display a conversation
|
|
3163
|
-
* @param {string} conversationId - Conversation ID
|
|
3164
|
-
*/
|
|
3165
|
-
async selectConversation(conversationId) {
|
|
3166
|
-
// Update selected conversation state
|
|
3167
|
-
this.selectedConversationId = conversationId;
|
|
3168
|
-
|
|
3169
|
-
// Update UI to show selection
|
|
3170
|
-
this.updateSelectedConversation();
|
|
3171
|
-
|
|
3172
|
-
// Load and display conversation messages
|
|
3173
|
-
await this.loadConversationMessages(conversationId);
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
/**
|
|
3177
|
-
* Update selected conversation in sidebar
|
|
3178
|
-
*/
|
|
3179
|
-
updateSelectedConversation() {
|
|
3180
|
-
// Remove previous selection
|
|
3181
|
-
const previousSelected = this.container.querySelector('.sidebar-conversation-item.selected');
|
|
3182
|
-
if (previousSelected) {
|
|
3183
|
-
previousSelected.classList.remove('selected');
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
// Add selection to current item
|
|
3187
|
-
const currentItem = this.container.querySelector(`[data-id="${this.selectedConversationId}"]`);
|
|
3188
|
-
if (currentItem) {
|
|
3189
|
-
currentItem.classList.add('selected');
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
|
-
// Update header with conversation info
|
|
3193
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
3194
|
-
const conversation = conversations.find(conv => conv.id === this.selectedConversationId);
|
|
3195
|
-
|
|
3196
|
-
if (conversation) {
|
|
3197
|
-
const titleElement = this.container.querySelector('#selected-conversation-title');
|
|
3198
|
-
const metaElement = this.container.querySelector('#selected-conversation-meta');
|
|
3199
|
-
|
|
3200
|
-
if (titleElement) {
|
|
3201
|
-
const baseTitle = conversation.title || `Chat ${conversation.id.slice(-8)}`;
|
|
3202
|
-
const agentName = this.getAgentNameForConversation(conversation.id);
|
|
3203
|
-
const agentColor = this.getAgentColorForConversation(conversation.id);
|
|
3204
|
-
|
|
3205
|
-
if (agentName && agentColor) {
|
|
3206
|
-
titleElement.innerHTML = `
|
|
3207
|
-
<span style="color: ${agentColor}; border-left: 3px solid ${agentColor}; padding-left: 8px;">
|
|
3208
|
-
${baseTitle}
|
|
3209
|
-
</span>
|
|
3210
|
-
<span class="agent-badge" style="background-color: ${agentColor};" title="Using ${agentName} agent">
|
|
3211
|
-
🤖 ${agentName}
|
|
3212
|
-
</span>
|
|
3213
|
-
`;
|
|
3214
|
-
} else {
|
|
3215
|
-
titleElement.textContent = baseTitle;
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
if (metaElement) {
|
|
3220
|
-
const messageCount = conversation.messageCount || 0;
|
|
3221
|
-
const lastActivity = this.formatRelativeTime(new Date(conversation.lastModified));
|
|
3222
|
-
metaElement.innerHTML = `
|
|
3223
|
-
<span class="meta-item">
|
|
3224
|
-
<span class="meta-icon">📁</span>
|
|
3225
|
-
${conversation.project || 'Unknown Project'}
|
|
3226
|
-
</span>
|
|
3227
|
-
<span class="meta-item">
|
|
3228
|
-
<span class="meta-icon">💬</span>
|
|
3229
|
-
${messageCount} message${messageCount !== 1 ? 's' : ''}
|
|
3230
|
-
</span>
|
|
3231
|
-
<span class="meta-item">
|
|
3232
|
-
<span class="meta-icon">🕒</span>
|
|
3233
|
-
${lastActivity}
|
|
3234
|
-
</span>
|
|
3235
|
-
`;
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
// Show and update the state banner
|
|
3240
|
-
this.showStateBanner(this.selectedConversationId);
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
/**
|
|
3244
|
-
* Load and display conversation messages (with caching)
|
|
3245
|
-
* @param {string} conversationId - Conversation ID
|
|
3246
|
-
*/
|
|
3247
|
-
async loadConversationMessages(conversationId) {
|
|
3248
|
-
// Reset pagination for new conversation
|
|
3249
|
-
this.messagesPagination = {
|
|
3250
|
-
currentPage: 0,
|
|
3251
|
-
limit: 10,
|
|
3252
|
-
hasMore: true,
|
|
3253
|
-
isLoading: false,
|
|
3254
|
-
conversationId: conversationId
|
|
3255
|
-
};
|
|
3256
|
-
|
|
3257
|
-
// Clear cached messages for this conversation
|
|
3258
|
-
this.loadedMessages.delete(conversationId);
|
|
3259
|
-
|
|
3260
|
-
// Load first page of messages
|
|
3261
|
-
await this.loadMoreMessages(conversationId, true);
|
|
3262
|
-
}
|
|
3263
|
-
|
|
3264
|
-
/**
|
|
3265
|
-
* Show and update conversation state banner
|
|
3266
|
-
* @param {string} conversationId - Conversation ID
|
|
3267
|
-
*/
|
|
3268
|
-
showStateBanner(conversationId) {
|
|
3269
|
-
const banner = this.container.querySelector('#conversation-state-banner');
|
|
3270
|
-
if (!banner) return;
|
|
3271
|
-
|
|
3272
|
-
// Show the banner
|
|
3273
|
-
banner.style.display = 'flex';
|
|
3274
|
-
|
|
3275
|
-
// Get current state from WebSocket or cache
|
|
3276
|
-
const conversationStates = this.stateService.getStateProperty('conversationStates') || {};
|
|
3277
|
-
const currentState = conversationStates[conversationId] || 'unknown';
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
// If we don't have the state yet, try to fetch it after a short delay
|
|
3281
|
-
if (currentState === 'unknown') {
|
|
3282
|
-
setTimeout(() => {
|
|
3283
|
-
this.fetchConversationState(conversationId);
|
|
3284
|
-
}, 100);
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
// Update banner with current state
|
|
3288
|
-
this.updateStateBanner(conversationId, currentState);
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
/**
|
|
3292
|
-
* Update conversation state banner
|
|
3293
|
-
* @param {string} conversationId - Conversation ID
|
|
3294
|
-
* @param {string} state - Current conversation state
|
|
3295
|
-
*/
|
|
3296
|
-
updateStateBanner(conversationId, state) {
|
|
3297
|
-
const banner = this.container.querySelector('#conversation-state-banner');
|
|
3298
|
-
const stateDot = this.container.querySelector('#state-dot');
|
|
3299
|
-
const stateText = this.container.querySelector('#state-text');
|
|
3300
|
-
const stateTimestamp = this.container.querySelector('#state-timestamp');
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
if (!banner || !stateDot || !stateText || !stateTimestamp) {
|
|
3304
|
-
return;
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
// Map states to user-friendly messages with enhanced descriptions
|
|
3308
|
-
const stateMessages = {
|
|
3309
|
-
'Claude Code working...': {
|
|
3310
|
-
text: '🤖 Claude is thinking and working...',
|
|
3311
|
-
description: 'Claude is processing your request',
|
|
3312
|
-
class: 'status-working',
|
|
3313
|
-
icon: '🧠'
|
|
3314
|
-
},
|
|
3315
|
-
'Awaiting tool response...': {
|
|
3316
|
-
text: '⚡ Waiting for tool execution...',
|
|
3317
|
-
description: 'Claude is waiting for tool results',
|
|
3318
|
-
class: 'status-tool-pending',
|
|
3319
|
-
icon: '🔧'
|
|
3320
|
-
},
|
|
3321
|
-
'Executing tools...': {
|
|
3322
|
-
text: '🔧 Executing tools...',
|
|
3323
|
-
description: 'Claude is running system tools',
|
|
3324
|
-
class: 'status-tool-executing',
|
|
3325
|
-
icon: '⚡'
|
|
3326
|
-
},
|
|
3327
|
-
'Analyzing results...': {
|
|
3328
|
-
text: '📊 Analyzing tool results...',
|
|
3329
|
-
description: 'Claude is processing tool outputs',
|
|
3330
|
-
class: 'status-analyzing',
|
|
3331
|
-
icon: '🔍'
|
|
3332
|
-
},
|
|
3333
|
-
'Analyzing code...': {
|
|
3334
|
-
text: '🔍 Analyzing code...',
|
|
3335
|
-
description: 'Claude is examining code or files',
|
|
3336
|
-
class: 'status-analyzing',
|
|
3337
|
-
icon: '📝'
|
|
3338
|
-
},
|
|
3339
|
-
'Fetching data...': {
|
|
3340
|
-
text: '🌐 Fetching data...',
|
|
3341
|
-
description: 'Claude is retrieving web content or external data',
|
|
3342
|
-
class: 'status-fetching',
|
|
3343
|
-
icon: '📶'
|
|
3344
|
-
},
|
|
3345
|
-
'Task completed': {
|
|
3346
|
-
text: '✅ Task completed',
|
|
3347
|
-
description: 'Claude has finished the requested task',
|
|
3348
|
-
class: 'status-completed',
|
|
3349
|
-
icon: '✨'
|
|
3350
|
-
},
|
|
3351
|
-
'Processing request...': {
|
|
3352
|
-
text: '⚙️ Processing request...',
|
|
3353
|
-
description: 'Claude is working on a complex request',
|
|
3354
|
-
class: 'status-processing',
|
|
3355
|
-
icon: '🔄'
|
|
3356
|
-
},
|
|
3357
|
-
'Encountered issue': {
|
|
3358
|
-
text: '⚠️ Encountered issue',
|
|
3359
|
-
description: 'Claude found an error or problem',
|
|
3360
|
-
class: 'status-error',
|
|
3361
|
-
icon: '🚟'
|
|
3362
|
-
},
|
|
3363
|
-
'Awaiting user input...': {
|
|
3364
|
-
text: '💬 Awaiting your input',
|
|
3365
|
-
description: 'Claude needs your response to continue',
|
|
3366
|
-
class: 'status-waiting',
|
|
3367
|
-
icon: '💭'
|
|
3368
|
-
},
|
|
3369
|
-
'Waiting for your response': {
|
|
3370
|
-
text: '💬 Waiting for your response',
|
|
3371
|
-
description: 'Claude is ready for your next message',
|
|
3372
|
-
class: 'status-waiting-response',
|
|
3373
|
-
icon: '📝'
|
|
3374
|
-
},
|
|
3375
|
-
'Awaiting response...': {
|
|
3376
|
-
text: '⏳ Awaiting Claude response',
|
|
3377
|
-
description: 'Waiting for Claude to respond',
|
|
3378
|
-
class: 'status-waiting',
|
|
3379
|
-
icon: '🤔'
|
|
3380
|
-
},
|
|
3381
|
-
'Recently active': {
|
|
3382
|
-
text: '🟢 Recently active',
|
|
3383
|
-
description: 'Conversation was active recently',
|
|
3384
|
-
class: 'status-active',
|
|
3385
|
-
icon: '✨'
|
|
3386
|
-
},
|
|
3387
|
-
'Idle': {
|
|
3388
|
-
text: '😴 Conversation idle',
|
|
3389
|
-
description: 'No recent activity',
|
|
3390
|
-
class: 'status-idle',
|
|
3391
|
-
icon: '💤'
|
|
3392
|
-
},
|
|
3393
|
-
'Inactive': {
|
|
3394
|
-
text: '⚪ Inactive',
|
|
3395
|
-
description: 'Conversation has been inactive',
|
|
3396
|
-
class: 'status-idle',
|
|
3397
|
-
icon: '⏸️'
|
|
3398
|
-
},
|
|
3399
|
-
'Old': {
|
|
3400
|
-
text: '📚 Archived conversation',
|
|
3401
|
-
description: 'No recent activity in this conversation',
|
|
3402
|
-
class: 'status-idle',
|
|
3403
|
-
icon: '📁'
|
|
3404
|
-
},
|
|
3405
|
-
'unknown': {
|
|
3406
|
-
text: '🔄 Loading conversation state...',
|
|
3407
|
-
description: 'Determining conversation status',
|
|
3408
|
-
class: 'status-loading',
|
|
3409
|
-
icon: '⏳'
|
|
3410
|
-
}
|
|
3411
|
-
};
|
|
3412
|
-
|
|
3413
|
-
const stateInfo = stateMessages[state] || stateMessages['unknown'];
|
|
3414
|
-
|
|
3415
|
-
// Update dot class with enhanced styling
|
|
3416
|
-
stateDot.className = `state-dot ${stateInfo.class}`;
|
|
3417
|
-
|
|
3418
|
-
// Check for agent usage
|
|
3419
|
-
const agentName = this.getAgentNameForConversation(conversationId);
|
|
3420
|
-
const agentColor = this.getAgentColorForConversation(conversationId);
|
|
3421
|
-
|
|
3422
|
-
// Update text with icon, description, and agent info
|
|
3423
|
-
let stateTextContent = stateInfo.text;
|
|
3424
|
-
let stateDescriptionContent = stateInfo.description;
|
|
3425
|
-
|
|
3426
|
-
// If an agent is detected and state indicates work, update the message
|
|
3427
|
-
if (agentName && (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing'))) {
|
|
3428
|
-
stateTextContent = `🤖 ${agentName} agent working...`;
|
|
3429
|
-
stateDescriptionContent = `The ${agentName} agent is processing your request`;
|
|
3430
|
-
|
|
3431
|
-
// Apply agent color to the dot
|
|
3432
|
-
if (agentColor) {
|
|
3433
|
-
stateDot.style.backgroundColor = agentColor;
|
|
3434
|
-
stateDot.style.borderColor = agentColor;
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
|
|
3438
|
-
stateText.innerHTML = `
|
|
3439
|
-
<span class="state-text-main">${stateTextContent}</span>
|
|
3440
|
-
<span class="state-text-description">${stateDescriptionContent}</span>
|
|
3441
|
-
`;
|
|
3442
|
-
|
|
3443
|
-
// Add tooltip for additional context
|
|
3444
|
-
stateText.title = stateInfo.description;
|
|
3445
|
-
|
|
3446
|
-
// Update timestamp with more context
|
|
3447
|
-
const now = new Date();
|
|
3448
|
-
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
3449
|
-
stateTimestamp.innerHTML = `
|
|
3450
|
-
<span class="timestamp-label">Last updated:</span>
|
|
3451
|
-
<span class="timestamp-value">${timeString}</span>
|
|
3452
|
-
`;
|
|
3453
|
-
|
|
3454
|
-
// Add pulsing animation for active states
|
|
3455
|
-
if (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing')) {
|
|
3456
|
-
stateDot.classList.add('pulse-animation');
|
|
3457
|
-
setTimeout(() => stateDot.classList.remove('pulse-animation'), 3000);
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
/**
|
|
3463
|
-
* Fetch conversation state from API
|
|
3464
|
-
* @param {string} conversationId - Conversation ID
|
|
3465
|
-
*/
|
|
3466
|
-
async fetchConversationState(conversationId) {
|
|
3467
|
-
try {
|
|
3468
|
-
const stateData = await this.dataService.getConversationStates();
|
|
3469
|
-
|
|
3470
|
-
if (stateData && stateData.activeStates && stateData.activeStates[conversationId]) {
|
|
3471
|
-
const state = stateData.activeStates[conversationId];
|
|
3472
|
-
|
|
3473
|
-
// Update the StateService with the new data
|
|
3474
|
-
this.stateService.updateConversationStates(stateData.activeStates);
|
|
3475
|
-
|
|
3476
|
-
// Update the banner with the real state
|
|
3477
|
-
this.updateStateBanner(conversationId, state);
|
|
3478
|
-
} else {
|
|
3479
|
-
// Keep showing unknown for now
|
|
3480
|
-
}
|
|
3481
|
-
} catch (error) {
|
|
3482
|
-
console.error('Error fetching conversation state:', error);
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
|
|
3486
|
-
/**
|
|
3487
|
-
* Hide conversation state banner
|
|
3488
|
-
*/
|
|
3489
|
-
hideStateBanner() {
|
|
3490
|
-
const banner = this.container.querySelector('#conversation-state-banner');
|
|
3491
|
-
if (banner) {
|
|
3492
|
-
banner.style.display = 'none';
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
/**
|
|
3497
|
-
* Auto-scroll to bottom of messages
|
|
3498
|
-
*/
|
|
3499
|
-
scrollToBottom() {
|
|
3500
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
3501
|
-
if (messagesContent) {
|
|
3502
|
-
messagesContent.scrollTop = messagesContent.scrollHeight;
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
/**
|
|
3507
|
-
* Show notification for new message
|
|
3508
|
-
* @param {Object} message - New message object
|
|
3509
|
-
* @param {Object} metadata - Message metadata
|
|
3510
|
-
*/
|
|
3511
|
-
showNewMessageNotification(message, metadata) {
|
|
3512
|
-
// Update banner if it's showing to reflect new activity
|
|
3513
|
-
if (this.selectedConversationId) {
|
|
3514
|
-
const banner = this.container.querySelector('#conversation-state-banner');
|
|
3515
|
-
if (banner && banner.style.display !== 'none') {
|
|
3516
|
-
// Temporarily highlight the banner to show activity
|
|
3517
|
-
banner.style.backgroundColor = 'rgba(213, 116, 85, 0.1)';
|
|
3518
|
-
setTimeout(() => {
|
|
3519
|
-
banner.style.backgroundColor = '';
|
|
3520
|
-
}, 1000);
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
// Could add visual indicator for new message (pulse, notification badge, etc.)
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
/**
|
|
3528
|
-
* Load more messages (for infinite scroll)
|
|
3529
|
-
* @param {string} conversationId - Conversation ID
|
|
3530
|
-
* @param {boolean} isInitialLoad - Whether this is the initial load
|
|
3531
|
-
*/
|
|
3532
|
-
async loadMoreMessages(conversationId, isInitialLoad = false) {
|
|
3533
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
3534
|
-
if (!messagesContent) return;
|
|
3535
|
-
|
|
3536
|
-
// Prevent concurrent loading
|
|
3537
|
-
if (this.messagesPagination.isLoading || !this.messagesPagination.hasMore) {
|
|
3538
|
-
return;
|
|
3539
|
-
}
|
|
3540
|
-
|
|
3541
|
-
// Ensure we're loading for the correct conversation
|
|
3542
|
-
if (this.messagesPagination.conversationId !== conversationId) {
|
|
3543
|
-
return;
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
try {
|
|
3547
|
-
this.messagesPagination.isLoading = true;
|
|
3548
|
-
|
|
3549
|
-
if (isInitialLoad) {
|
|
3550
|
-
// Show loading state for initial load
|
|
3551
|
-
messagesContent.innerHTML = `
|
|
3552
|
-
<div class="messages-loading">
|
|
3553
|
-
<div class="loading-spinner"></div>
|
|
3554
|
-
<span>Loading messages...</span>
|
|
3555
|
-
</div>
|
|
3556
|
-
`;
|
|
3557
|
-
} else {
|
|
3558
|
-
// Show loading indicator at top for infinite scroll
|
|
3559
|
-
this.showMessagesLoadingIndicator(true);
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
// Fetch paginated messages from the server
|
|
3563
|
-
const messagesData = await this.dataService.cachedFetch(
|
|
3564
|
-
`/api/conversations/${conversationId}/messages?page=${this.messagesPagination.currentPage}&limit=${this.messagesPagination.limit}`
|
|
3565
|
-
);
|
|
3566
|
-
|
|
3567
|
-
if (messagesData && messagesData.messages) {
|
|
3568
|
-
// Update pagination state - handle both paginated and non-paginated responses
|
|
3569
|
-
if (messagesData.pagination) {
|
|
3570
|
-
// Paginated response
|
|
3571
|
-
this.messagesPagination.hasMore = messagesData.pagination.hasMore;
|
|
3572
|
-
this.messagesPagination.currentPage = messagesData.pagination.page + 1;
|
|
3573
|
-
} else {
|
|
3574
|
-
// Non-paginated response (fallback) - treat as complete data
|
|
3575
|
-
this.messagesPagination.hasMore = false;
|
|
3576
|
-
this.messagesPagination.currentPage = 1;
|
|
3577
|
-
}
|
|
3578
|
-
|
|
3579
|
-
// Get existing messages or initialize
|
|
3580
|
-
let existingMessages = this.loadedMessages.get(conversationId) || [];
|
|
3581
|
-
|
|
3582
|
-
if (isInitialLoad) {
|
|
3583
|
-
// For initial load, replace all messages
|
|
3584
|
-
existingMessages = messagesData.messages;
|
|
3585
|
-
} else {
|
|
3586
|
-
// For infinite scroll, prepend older messages (they come in chronological order)
|
|
3587
|
-
existingMessages = [...messagesData.messages, ...existingMessages];
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
// Cache the combined messages
|
|
3591
|
-
this.loadedMessages.set(conversationId, existingMessages);
|
|
3592
|
-
|
|
3593
|
-
// Render messages
|
|
3594
|
-
this.renderCachedMessages(existingMessages, !isInitialLoad);
|
|
3595
|
-
|
|
3596
|
-
// Setup scroll listener for infinite scroll (only on initial load)
|
|
3597
|
-
if (isInitialLoad) {
|
|
3598
|
-
this.setupMessagesScrollListener(conversationId);
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
} else if (isInitialLoad) {
|
|
3603
|
-
messagesContent.innerHTML = `
|
|
3604
|
-
<div class="no-messages-found">
|
|
3605
|
-
<div class="no-messages-icon">💭</div>
|
|
3606
|
-
<h4>No messages found</h4>
|
|
3607
|
-
<p>This conversation has no messages or they could not be loaded.</p>
|
|
3608
|
-
</div>
|
|
3609
|
-
`;
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
} catch (error) {
|
|
3613
|
-
console.error('Error loading messages:', error);
|
|
3614
|
-
|
|
3615
|
-
if (isInitialLoad) {
|
|
3616
|
-
messagesContent.innerHTML = `
|
|
3617
|
-
<div class="messages-error">
|
|
3618
|
-
<span class="error-icon">⚠️</span>
|
|
3619
|
-
<span>Failed to load messages</span>
|
|
3620
|
-
<button class="retry-messages" data-conversation-id="${conversationId}">Retry</button>
|
|
3621
|
-
</div>
|
|
3622
|
-
`;
|
|
3623
|
-
|
|
3624
|
-
// Bind retry button event
|
|
3625
|
-
const retryBtn = messagesContent.querySelector('.retry-messages');
|
|
3626
|
-
if (retryBtn) {
|
|
3627
|
-
retryBtn.addEventListener('click', () => {
|
|
3628
|
-
this.loadConversationMessages(conversationId);
|
|
3629
|
-
});
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
} finally {
|
|
3633
|
-
this.messagesPagination.isLoading = false;
|
|
3634
|
-
if (!isInitialLoad) {
|
|
3635
|
-
this.showMessagesLoadingIndicator(false);
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
|
-
/**
|
|
3641
|
-
* Render cached messages
|
|
3642
|
-
* @param {Array} messages - Array of messages
|
|
3643
|
-
* @param {boolean} prepend - Whether to prepend messages (for infinite scroll)
|
|
3644
|
-
*/
|
|
3645
|
-
renderCachedMessages(messages, prepend = false) {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
3649
|
-
if (!messagesContent) {
|
|
3650
|
-
console.warn(`⚠️ messages-content element not found!`);
|
|
3651
|
-
return;
|
|
3652
|
-
}
|
|
3653
|
-
|
|
3654
|
-
// Store messages globally for tool result lookup
|
|
3655
|
-
if (typeof window !== 'undefined') {
|
|
3656
|
-
window.currentMessages = messages;
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
const messageHTML = `
|
|
3660
|
-
<div class="messages-loading-indicator" style="display: none;">
|
|
3661
|
-
<div class="loading-spinner small"></div>
|
|
3662
|
-
<span>Loading older messages...</span>
|
|
3663
|
-
</div>
|
|
3664
|
-
<div class="messages-list">
|
|
3665
|
-
${messages.map(msg => this.renderMessage(msg)).join('')}
|
|
3666
|
-
</div>
|
|
3667
|
-
`;
|
|
3668
|
-
|
|
3669
|
-
if (prepend) {
|
|
3670
|
-
// For infinite scroll, we need to maintain scroll position
|
|
3671
|
-
const oldScrollHeight = messagesContent.scrollHeight;
|
|
3672
|
-
|
|
3673
|
-
// Update content
|
|
3674
|
-
messagesContent.innerHTML = messageHTML;
|
|
3675
|
-
|
|
3676
|
-
// Restore scroll position relative to the bottom
|
|
3677
|
-
const newScrollHeight = messagesContent.scrollHeight;
|
|
3678
|
-
const scrollDifference = newScrollHeight - oldScrollHeight;
|
|
3679
|
-
messagesContent.scrollTop += scrollDifference;
|
|
3680
|
-
} else {
|
|
3681
|
-
// Initial load - just replace content and scroll to bottom
|
|
3682
|
-
messagesContent.innerHTML = messageHTML;
|
|
3683
|
-
|
|
3684
|
-
// Scroll to bottom for new conversation load
|
|
3685
|
-
setTimeout(() => {
|
|
3686
|
-
messagesContent.scrollTop = messagesContent.scrollHeight;
|
|
3687
|
-
}, 100);
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
// Bind tool display events
|
|
3691
|
-
this.toolDisplay.bindEvents(messagesContent);
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
/**
|
|
3695
|
-
* Show/hide messages loading indicator
|
|
3696
|
-
* @param {boolean} show - Whether to show the indicator
|
|
3697
|
-
*/
|
|
3698
|
-
showMessagesLoadingIndicator(show) {
|
|
3699
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
3700
|
-
if (!messagesContent) return;
|
|
3701
|
-
|
|
3702
|
-
const indicator = messagesContent.querySelector('.messages-loading-indicator');
|
|
3703
|
-
if (indicator) {
|
|
3704
|
-
indicator.style.display = show ? 'flex' : 'none';
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
/**
|
|
3709
|
-
* Setup scroll listener for infinite scroll in messages
|
|
3710
|
-
* @param {string} conversationId - Current conversation ID
|
|
3711
|
-
*/
|
|
3712
|
-
setupMessagesScrollListener(conversationId) {
|
|
3713
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
3714
|
-
if (!messagesContent) return;
|
|
3715
|
-
|
|
3716
|
-
// Remove existing listener if any
|
|
3717
|
-
if (this.messagesScrollListener) {
|
|
3718
|
-
messagesContent.removeEventListener('scroll', this.messagesScrollListener);
|
|
3719
|
-
}
|
|
3720
|
-
|
|
3721
|
-
// Create new listener
|
|
3722
|
-
this.messagesScrollListener = () => {
|
|
3723
|
-
// Check if we've scrolled near the top (for loading older messages)
|
|
3724
|
-
const scrollTop = messagesContent.scrollTop;
|
|
3725
|
-
const threshold = 100; // pixels from top
|
|
3726
|
-
|
|
3727
|
-
if (scrollTop <= threshold && this.messagesPagination.hasMore && !this.messagesPagination.isLoading) {
|
|
3728
|
-
this.loadMoreMessages(conversationId, false);
|
|
3729
|
-
}
|
|
3730
|
-
};
|
|
3731
|
-
|
|
3732
|
-
// Add listener
|
|
3733
|
-
messagesContent.addEventListener('scroll', this.messagesScrollListener);
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
/**
|
|
3737
|
-
* Render a single message with terminal-style formatting
|
|
3738
|
-
* @param {Object} message - Message object
|
|
3739
|
-
* @returns {string} HTML string
|
|
3740
|
-
*/
|
|
3741
|
-
renderMessage(message) {
|
|
3742
|
-
const timestamp = this.formatRelativeTime(new Date(message.timestamp));
|
|
3743
|
-
const fullTimestamp = new Date(message.timestamp).toLocaleString();
|
|
3744
|
-
// Compact summaries should be displayed as assistant messages even if marked as 'user'
|
|
3745
|
-
const isUser = message.role === 'user' && !message.isCompactSummary;
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
// Detect if message contains tools
|
|
3749
|
-
const hasTools = Array.isArray(message.content) &&
|
|
3750
|
-
message.content.some(block => block.type === 'tool_use');
|
|
3751
|
-
const toolCount = hasTools ?
|
|
3752
|
-
message.content.filter(block => block.type === 'tool_use').length : 0;
|
|
3753
|
-
|
|
3754
|
-
// Terminal-style prompt
|
|
3755
|
-
const prompt = isUser ? '>' : '#';
|
|
3756
|
-
const roleLabel = isUser ? 'user' : 'claude';
|
|
3757
|
-
|
|
3758
|
-
// Get message ID (short version for display)
|
|
3759
|
-
const messageId = message.id ? message.id.slice(-8) : 'unknown';
|
|
3760
|
-
|
|
3761
|
-
return `
|
|
3762
|
-
<div class="terminal-message ${isUser ? 'user' : 'assistant'}" data-message-id="${message.id || ''}">
|
|
3763
|
-
<div class="message-container">
|
|
3764
|
-
<div class="message-prompt">
|
|
3765
|
-
<span class="prompt-char">${prompt}</span>
|
|
3766
|
-
<div class="message-metadata">
|
|
3767
|
-
<span class="timestamp" title="${fullTimestamp}">${timestamp}</span>
|
|
3768
|
-
<span class="role-label">${roleLabel}</span>
|
|
3769
|
-
<span class="message-id" title="Message ID: ${message.id || 'unknown'}">[${messageId}]</span>
|
|
3770
|
-
${message.usage ? `
|
|
3771
|
-
<span class="tokens">
|
|
3772
|
-
${message.usage.input_tokens > 0 ? `i:${message.usage.input_tokens}` : ''}
|
|
3773
|
-
${message.usage.output_tokens > 0 ? `o:${message.usage.output_tokens}` : ''}
|
|
3774
|
-
${message.usage.cache_read_input_tokens > 0 ? `c:${message.usage.cache_read_input_tokens}` : ''}
|
|
3775
|
-
</span>
|
|
3776
|
-
` : ''}
|
|
3777
|
-
${hasTools ? `<span class="tool-count">[${toolCount}t]</span>` : ''}
|
|
3778
|
-
${message.model ? `<span class="model">[${message.model.replace('claude-', '').replace('-20250514', '')}]</span>` : ''}
|
|
3779
|
-
</div>
|
|
3780
|
-
</div>
|
|
3781
|
-
<div class="message-body">
|
|
3782
|
-
${this.formatMessageContent(message.content, message)}
|
|
3783
|
-
</div>
|
|
3784
|
-
</div>
|
|
3785
|
-
</div>
|
|
3786
|
-
`;
|
|
3787
|
-
}
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
/**
|
|
3791
|
-
* Format message content with support for text and tool calls
|
|
3792
|
-
* @param {string|Array} content - Message content
|
|
3793
|
-
* @returns {string} Formatted HTML
|
|
3794
|
-
*/
|
|
3795
|
-
formatMessageContent(content, message = null) {
|
|
3796
|
-
let result = '';
|
|
3797
|
-
|
|
3798
|
-
// Handle different content formats
|
|
3799
|
-
if (Array.isArray(content)) {
|
|
3800
|
-
// Assistant messages with content blocks
|
|
3801
|
-
content.forEach((block, index) => {
|
|
3802
|
-
if (block.type === 'text') {
|
|
3803
|
-
result += this.formatTextContent(block.text);
|
|
3804
|
-
} else if (block.type === 'tool_use') {
|
|
3805
|
-
// Log only tool rendering for debugging
|
|
3806
|
-
console.log('🔧 WebSocket: Rendering tool', { name: block.name, hasResults: !!message?.toolResults });
|
|
3807
|
-
result += this.toolDisplay.renderToolUse(block, message?.toolResults);
|
|
3808
|
-
} else if (block.type === 'tool_result') {
|
|
3809
|
-
result += this.toolDisplay.renderToolResult(block);
|
|
3810
|
-
}
|
|
3811
|
-
});
|
|
3812
|
-
} else if (typeof content === 'string' && content.trim() !== '') {
|
|
3813
|
-
// User messages with plain text - check for special patterns
|
|
3814
|
-
if (content.includes('Tool Result') && content.length > 1000) {
|
|
3815
|
-
// This is likely a large tool result that should be handled specially
|
|
3816
|
-
result += this.formatLargeToolResult(content);
|
|
3817
|
-
} else {
|
|
3818
|
-
// Check if this is a confirmation response "[ok]" or similar
|
|
3819
|
-
const enhancedContent = this.enhanceConfirmationMessage(content, message);
|
|
3820
|
-
result = this.formatTextContent(enhancedContent);
|
|
3821
|
-
}
|
|
3822
|
-
} else if (content && typeof content === 'object') {
|
|
3823
|
-
// Handle edge cases where content might be an object
|
|
3824
|
-
result = this.formatTextContent(JSON.stringify(content, null, 2));
|
|
3825
|
-
}
|
|
3826
|
-
|
|
3827
|
-
return result || '<em class="empty-content">No displayable content available</em>';
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
|
-
/**
|
|
3831
|
-
* Format regular text content with enhanced Markdown support
|
|
3832
|
-
* @param {string} text - Text content
|
|
3833
|
-
* @returns {string} Formatted HTML
|
|
3834
|
-
*/
|
|
3835
|
-
/**
|
|
3836
|
-
* Apply markdown formatting to HTML-escaped text
|
|
3837
|
-
* @param {string} escapedText - HTML-escaped text to format
|
|
3838
|
-
* @returns {string} Formatted text with markdown styling
|
|
3839
|
-
*/
|
|
3840
|
-
applyMarkdownFormatting(escapedText) {
|
|
3841
|
-
let formattedText = escapedText;
|
|
3842
|
-
|
|
3843
|
-
// 1. Code blocks (must be first to avoid conflicts)
|
|
3844
|
-
formattedText = formattedText
|
|
3845
|
-
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre class="code-block" data-language="$1"><code>$2</code></pre>');
|
|
3846
|
-
|
|
3847
|
-
// 2. Headers (h1-h6)
|
|
3848
|
-
formattedText = formattedText
|
|
3849
|
-
.replace(/^### (.*$)/gm, '<h3 class="markdown-h3">$1</h3>')
|
|
3850
|
-
.replace(/^## (.*$)/gm, '<h2 class="markdown-h2">$1</h2>')
|
|
3851
|
-
.replace(/^# (.*$)/gm, '<h1 class="markdown-h1">$1</h1>')
|
|
3852
|
-
.replace(/^#### (.*$)/gm, '<h4 class="markdown-h4">$1</h4>')
|
|
3853
|
-
.replace(/^##### (.*$)/gm, '<h5 class="markdown-h5">$1</h5>')
|
|
3854
|
-
.replace(/^###### (.*$)/gm, '<h6 class="markdown-h6">$1</h6>');
|
|
3855
|
-
|
|
3856
|
-
// 3. Bold and italic text
|
|
3857
|
-
formattedText = formattedText
|
|
3858
|
-
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
|
|
3859
|
-
.replace(/\*\*(.*?)\*\*/g, '<strong class="markdown-bold">$1</strong>')
|
|
3860
|
-
.replace(/\*(.*?)\*/g, '<em class="markdown-italic">$1</em>')
|
|
3861
|
-
.replace(/\_\_\_(.*?)\_\_\_/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
|
|
3862
|
-
.replace(/\_\_(.*?)\_\_/g, '<strong class="markdown-bold">$1</strong>')
|
|
3863
|
-
.replace(/\_(.*?)\_/g, '<em class="markdown-italic">$1</em>');
|
|
3864
|
-
|
|
3865
|
-
// 4. Strikethrough
|
|
3866
|
-
formattedText = formattedText
|
|
3867
|
-
.replace(/~~(.*?)~~/g, '<del class="markdown-strikethrough">$1</del>');
|
|
3868
|
-
|
|
3869
|
-
// 5. Links
|
|
3870
|
-
formattedText = formattedText
|
|
3871
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="markdown-link" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
3872
|
-
|
|
3873
|
-
// 6. Inline code (after other formatting to avoid conflicts)
|
|
3874
|
-
formattedText = formattedText
|
|
3875
|
-
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
3876
|
-
|
|
3877
|
-
// 7. Lists (unordered)
|
|
3878
|
-
formattedText = formattedText
|
|
3879
|
-
.replace(/^[\s]*[\*\-\+][\s]+(.*)$/gm, '<li class="markdown-list-item">$1</li>');
|
|
3880
|
-
|
|
3881
|
-
// 8. Lists (ordered)
|
|
3882
|
-
formattedText = formattedText
|
|
3883
|
-
.replace(/^[\s]*\d+\.[\s]+(.*)$/gm, '<li class="markdown-ordered-item">$1</li>');
|
|
3884
|
-
|
|
3885
|
-
// 9. Wrap consecutive list items in ul/ol tags
|
|
3886
|
-
formattedText = formattedText
|
|
3887
|
-
.replace(/(<li class="markdown-list-item">.*<\/li>)/gs, (match) => {
|
|
3888
|
-
return '<ul class="markdown-list">' + match + '</ul>';
|
|
3889
|
-
})
|
|
3890
|
-
.replace(/(<li class="markdown-ordered-item">.*<\/li>)/gs, (match) => {
|
|
3891
|
-
return '<ol class="markdown-ordered-list">' + match + '</ol>';
|
|
3892
|
-
});
|
|
3893
|
-
|
|
3894
|
-
// 10. Blockquotes
|
|
3895
|
-
formattedText = formattedText
|
|
3896
|
-
.replace(/^>[\s]*(.*)$/gm, '<blockquote class="markdown-blockquote">$1</blockquote>');
|
|
3897
|
-
|
|
3898
|
-
// 11. Horizontal rules
|
|
3899
|
-
formattedText = formattedText
|
|
3900
|
-
.replace(/^[\s]*---[\s]*$/gm, '<hr class="markdown-hr">')
|
|
3901
|
-
.replace(/^[\s]*\*\*\*[\s]*$/gm, '<hr class="markdown-hr">');
|
|
3902
|
-
|
|
3903
|
-
// 12. Line breaks (last to avoid conflicts)
|
|
3904
|
-
formattedText = formattedText
|
|
3905
|
-
.replace(/\n\n/g, '</p><p class="markdown-paragraph">')
|
|
3906
|
-
.replace(/\n/g, '<br>');
|
|
3907
|
-
|
|
3908
|
-
// 13. Wrap in paragraph if not already wrapped
|
|
3909
|
-
if (!formattedText.includes('<p') && !formattedText.includes('<h') &&
|
|
3910
|
-
!formattedText.includes('<ul') && !formattedText.includes('<ol') &&
|
|
3911
|
-
!formattedText.includes('<blockquote')) {
|
|
3912
|
-
formattedText = '<p class="markdown-paragraph">' + formattedText + '</p>';
|
|
3913
|
-
}
|
|
3914
|
-
|
|
3915
|
-
return formattedText;
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
formatTextContent(text) {
|
|
3919
|
-
if (!text || text.trim() === '') return '';
|
|
3920
|
-
|
|
3921
|
-
// Escape HTML to prevent XSS
|
|
3922
|
-
const escapeHtml = (str) => {
|
|
3923
|
-
const div = document.createElement('div');
|
|
3924
|
-
div.textContent = str;
|
|
3925
|
-
return div.innerHTML;
|
|
3926
|
-
};
|
|
3927
|
-
|
|
3928
|
-
// Check if text is too long and needs truncation
|
|
3929
|
-
const lines = text.split('\n');
|
|
3930
|
-
const maxVisibleLines = 20; // Increased from 5 to 20 for better visibility
|
|
3931
|
-
|
|
3932
|
-
if (lines.length > maxVisibleLines) {
|
|
3933
|
-
const visibleLines = lines.slice(0, maxVisibleLines);
|
|
3934
|
-
const hiddenLinesCount = lines.length - maxVisibleLines;
|
|
3935
|
-
const contentId = 'text_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
3936
|
-
|
|
3937
|
-
// Store full content for modal
|
|
3938
|
-
if (typeof window !== 'undefined') {
|
|
3939
|
-
window.storedContent = window.storedContent || {};
|
|
3940
|
-
window.storedContent[contentId] = text;
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
const previewText = escapeHtml(visibleLines.join('\n'));
|
|
3944
|
-
const showMoreButton = `<button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${hiddenLinesCount} lines</button>`;
|
|
3945
|
-
|
|
3946
|
-
// Apply markdown formatting to preview
|
|
3947
|
-
let formattedPreview = this.applyMarkdownFormatting(previewText);
|
|
3948
|
-
|
|
3949
|
-
return `<div class="text-content-preview">${formattedPreview}<div class="text-expand-section"><span class="continuation">… +${hiddenLinesCount} lines hidden</span> ${showMoreButton}</div></div>`;
|
|
3950
|
-
}
|
|
3951
|
-
|
|
3952
|
-
// For non-truncated content, apply full formatting
|
|
3953
|
-
let formattedText = escapeHtml(text);
|
|
3954
|
-
formattedText = this.applyMarkdownFormatting(formattedText);
|
|
3955
|
-
|
|
3956
|
-
return formattedText;
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
/**
|
|
3960
|
-
* Format large tool result content safely
|
|
3961
|
-
* @param {string} content - Large tool result content
|
|
3962
|
-
* @returns {string} Safe formatted content
|
|
3963
|
-
*/
|
|
3964
|
-
formatLargeToolResult(content) {
|
|
3965
|
-
// Extract tool result ID if present
|
|
3966
|
-
const toolIdMatch = content.match(/Tool Result\s+([A-Za-z0-9]+)/);
|
|
3967
|
-
const toolId = toolIdMatch ? toolIdMatch[1] : 'unknown';
|
|
3968
|
-
|
|
3969
|
-
const escapeHtml = (str) => {
|
|
3970
|
-
const div = document.createElement('div');
|
|
3971
|
-
div.textContent = str;
|
|
3972
|
-
return div.innerHTML;
|
|
3973
|
-
};
|
|
3974
|
-
|
|
3975
|
-
const preview = content.length > 80
|
|
3976
|
-
? escapeHtml(content.substring(0, 80)) + '...'
|
|
3977
|
-
: escapeHtml(content);
|
|
3978
|
-
|
|
3979
|
-
return `
|
|
3980
|
-
<div class="terminal-tool tool-result large">
|
|
3981
|
-
<span class="tool-prompt">></span>
|
|
3982
|
-
<span class="tool-status">[LARGE]</span>
|
|
3983
|
-
<span class="tool-id">[${toolId}]</span>
|
|
3984
|
-
<span class="tool-output">${content.length}b: ${preview}</span>
|
|
3985
|
-
</div>
|
|
3986
|
-
`;
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
|
-
/**
|
|
3990
|
-
* Enhance confirmation messages like "[ok]" with context information
|
|
3991
|
-
* @param {string} content - Original message content
|
|
3992
|
-
* @param {Object} message - Full message object with metadata
|
|
3993
|
-
* @returns {string} Enhanced message content
|
|
3994
|
-
*/
|
|
3995
|
-
enhanceConfirmationMessage(content, message) {
|
|
3996
|
-
const trimmedContent = content.trim();
|
|
3997
|
-
|
|
3998
|
-
// Detect simple confirmation patterns
|
|
3999
|
-
const confirmationPatterns = [
|
|
4000
|
-
/^\[ok\]$/i,
|
|
4001
|
-
/^ok$/i,
|
|
4002
|
-
/^yes$/i,
|
|
4003
|
-
/^\[yes\]$/i,
|
|
4004
|
-
/^y$/i,
|
|
4005
|
-
/^\[y\]$/i,
|
|
4006
|
-
/^1$/, // Choice selection
|
|
4007
|
-
/^2$/,
|
|
4008
|
-
/^3$/
|
|
4009
|
-
];
|
|
4010
|
-
|
|
4011
|
-
const isConfirmation = confirmationPatterns.some(pattern => pattern.test(trimmedContent));
|
|
4012
|
-
|
|
4013
|
-
if (isConfirmation && message) {
|
|
4014
|
-
// Try to extract context from the message timestamp
|
|
4015
|
-
const messageTime = message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : 'unknown time';
|
|
4016
|
-
|
|
4017
|
-
// Enhanced display for confirmation messages
|
|
4018
|
-
return `${content} <span class="confirmation-context">(User confirmation at ${messageTime})</span>`;
|
|
4019
|
-
}
|
|
4020
|
-
|
|
4021
|
-
// For other potential confirmation-like messages, check if they seem like choices
|
|
4022
|
-
if (/^[1-9]$/.test(trimmedContent)) {
|
|
4023
|
-
return `${content} <span class="confirmation-context">(Menu selection)</span>`;
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
|
-
// Check for common CLI responses
|
|
4027
|
-
if (/^(continue|proceed|accept|confirm|done)$/i.test(trimmedContent)) {
|
|
4028
|
-
return `${content} <span class="confirmation-context">(User command)</span>`;
|
|
4029
|
-
}
|
|
4030
|
-
|
|
4031
|
-
return content;
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
/**
|
|
4035
|
-
* Detect which agent is being used in a conversation
|
|
4036
|
-
* @param {string} conversationId - Conversation ID
|
|
4037
|
-
* @returns {Object|null} Agent info or null if no agent detected
|
|
4038
|
-
*/
|
|
4039
|
-
detectAgentInConversation(conversationId) {
|
|
4040
|
-
const messages = this.loadedMessages.get(conversationId) || [];
|
|
4041
|
-
|
|
4042
|
-
// Look for agent indicators in recent messages
|
|
4043
|
-
for (let i = messages.length - 1; i >= Math.max(0, messages.length - 10); i--) {
|
|
4044
|
-
const message = messages[i];
|
|
4045
|
-
|
|
4046
|
-
if (message.role === 'assistant' && message.content) {
|
|
4047
|
-
let contentText = '';
|
|
4048
|
-
|
|
4049
|
-
// Extract text content from message
|
|
4050
|
-
if (Array.isArray(message.content)) {
|
|
4051
|
-
contentText = message.content
|
|
4052
|
-
.filter(block => block.type === 'text')
|
|
4053
|
-
.map(block => block.text)
|
|
4054
|
-
.join(' ');
|
|
4055
|
-
} else if (typeof message.content === 'string') {
|
|
4056
|
-
contentText = message.content;
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
// Check for agent usage patterns
|
|
4060
|
-
const agentPatterns = [
|
|
4061
|
-
/use(?:s|d)?\s+the\s+([a-zA-Z0-9\-_]+)\s+(?:sub\s+)?agent/i,
|
|
4062
|
-
/([a-zA-Z0-9\-_]+)\s+agent\s+(?:to|for|will)/i,
|
|
4063
|
-
/delegat(?:e|ing)\s+(?:to|task|this)\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+agent/i,
|
|
4064
|
-
/invok(?:e|ing)\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+agent/i
|
|
4065
|
-
];
|
|
4066
|
-
|
|
4067
|
-
for (const pattern of agentPatterns) {
|
|
4068
|
-
const match = contentText.match(pattern);
|
|
4069
|
-
if (match) {
|
|
4070
|
-
const detectedAgentName = match[1].toLowerCase();
|
|
4071
|
-
|
|
4072
|
-
// Find matching agent from our loaded agents
|
|
4073
|
-
const agent = this.agents.find(a =>
|
|
4074
|
-
a.name.toLowerCase() === detectedAgentName ||
|
|
4075
|
-
a.name.toLowerCase().replace(/-/g, '') === detectedAgentName.replace(/-/g, '')
|
|
4076
|
-
);
|
|
4077
|
-
|
|
4078
|
-
if (agent) {
|
|
4079
|
-
return {
|
|
4080
|
-
agent,
|
|
4081
|
-
detectedAt: message.timestamp,
|
|
4082
|
-
confidence: 'high'
|
|
4083
|
-
};
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
return null;
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
/**
|
|
4094
|
-
* Get agent color for conversation
|
|
4095
|
-
* @param {string} conversationId - Conversation ID
|
|
4096
|
-
* @returns {string|null} Agent color or null
|
|
4097
|
-
*/
|
|
4098
|
-
getAgentColorForConversation(conversationId) {
|
|
4099
|
-
const agentInfo = this.detectAgentInConversation(conversationId);
|
|
4100
|
-
return agentInfo ? agentInfo.agent.color : null;
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
|
-
/**
|
|
4104
|
-
* Get agent name for conversation
|
|
4105
|
-
* @param {string} conversationId - Conversation ID
|
|
4106
|
-
* @returns {string|null} Agent name or null
|
|
4107
|
-
*/
|
|
4108
|
-
getAgentNameForConversation(conversationId) {
|
|
4109
|
-
const agentInfo = this.detectAgentInConversation(conversationId);
|
|
4110
|
-
return agentInfo ? agentInfo.agent.name : null;
|
|
4111
|
-
}
|
|
4112
|
-
|
|
4113
|
-
/**
|
|
4114
|
-
* Format relative time
|
|
4115
|
-
* @param {Date} date - Date to format
|
|
4116
|
-
* @returns {string} Relative time string
|
|
4117
|
-
*/
|
|
4118
|
-
formatRelativeTime(date) {
|
|
4119
|
-
const now = new Date();
|
|
4120
|
-
const diffMs = now - date;
|
|
4121
|
-
const diffSecs = Math.floor(diffMs / 1000);
|
|
4122
|
-
const diffMins = Math.floor(diffSecs / 60);
|
|
4123
|
-
const diffHours = Math.floor(diffMins / 60);
|
|
4124
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
4125
|
-
|
|
4126
|
-
if (diffSecs < 60) return 'Just now';
|
|
4127
|
-
if (diffMins < 60) return `${diffMins}m ago`;
|
|
4128
|
-
if (diffHours < 24) return `${diffHours}h ago`;
|
|
4129
|
-
if (diffDays < 7) return `${diffDays}d ago`;
|
|
4130
|
-
return date.toLocaleDateString();
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
/**
|
|
4134
|
-
* Update clear filters button visibility
|
|
4135
|
-
*/
|
|
4136
|
-
updateClearFiltersButton() {
|
|
4137
|
-
const clearBtn = this.container.querySelector('#clear-filters');
|
|
4138
|
-
if (!clearBtn) return; // Guard against null when AgentsPage isn't rendered
|
|
4139
|
-
|
|
4140
|
-
const hasActiveFilters = this.filters.status !== 'all' ||
|
|
4141
|
-
this.filters.timeRange !== '7d' ||
|
|
4142
|
-
this.filters.search !== '';
|
|
4143
|
-
clearBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
/**
|
|
4147
|
-
* Handle list action
|
|
4148
|
-
* @param {string} action - Action type
|
|
4149
|
-
* @param {string} conversationId - Conversation ID
|
|
4150
|
-
*/
|
|
4151
|
-
handleListAction(action, conversationId) {
|
|
4152
|
-
switch (action) {
|
|
4153
|
-
case 'view':
|
|
4154
|
-
this.viewConversation(conversationId);
|
|
4155
|
-
break;
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
|
-
|
|
4159
|
-
/**
|
|
4160
|
-
* Filter conversations based on current filters
|
|
4161
|
-
* @param {Array} conversations - All conversations
|
|
4162
|
-
* @param {Object} states - Conversation states
|
|
4163
|
-
* @returns {Array} Filtered conversations
|
|
4164
|
-
*/
|
|
4165
|
-
filterConversations(conversations, states) {
|
|
4166
|
-
let filtered = conversations;
|
|
4167
|
-
|
|
4168
|
-
// Filter by status
|
|
4169
|
-
if (this.filters.status !== 'all') {
|
|
4170
|
-
filtered = filtered.filter(conv => {
|
|
4171
|
-
const state = states[conv.id] || 'unknown';
|
|
4172
|
-
const category = this.getStateCategory(state);
|
|
4173
|
-
return category === this.filters.status;
|
|
4174
|
-
});
|
|
4175
|
-
}
|
|
4176
|
-
|
|
4177
|
-
// Filter by time range
|
|
4178
|
-
const timeRange = this.getTimeRangeMs(this.filters.timeRange);
|
|
4179
|
-
if (timeRange > 0) {
|
|
4180
|
-
const cutoff = Date.now() - timeRange;
|
|
4181
|
-
filtered = filtered.filter(conv => {
|
|
4182
|
-
const lastModified = new Date(conv.lastModified).getTime();
|
|
4183
|
-
return lastModified >= cutoff;
|
|
4184
|
-
});
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
// Filter by search
|
|
4188
|
-
if (this.filters.search) {
|
|
4189
|
-
const searchLower = this.filters.search.toLowerCase();
|
|
4190
|
-
filtered = filtered.filter(conv => {
|
|
4191
|
-
return (conv.title || '').toLowerCase().includes(searchLower) ||
|
|
4192
|
-
(conv.project || '').toLowerCase().includes(searchLower) ||
|
|
4193
|
-
(conv.lastMessage || '').toLowerCase().includes(searchLower);
|
|
4194
|
-
});
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
|
-
return filtered;
|
|
4198
|
-
}
|
|
4199
|
-
|
|
4200
|
-
/**
|
|
4201
|
-
* Get time range in milliseconds
|
|
4202
|
-
* @param {string} range - Time range string
|
|
4203
|
-
* @returns {number} Milliseconds
|
|
4204
|
-
*/
|
|
4205
|
-
getTimeRangeMs(range) {
|
|
4206
|
-
const ranges = {
|
|
4207
|
-
'1h': 60 * 60 * 1000,
|
|
4208
|
-
'24h': 24 * 60 * 60 * 1000,
|
|
4209
|
-
'7d': 7 * 24 * 60 * 60 * 1000,
|
|
4210
|
-
'30d': 30 * 24 * 60 * 60 * 1000
|
|
4211
|
-
};
|
|
4212
|
-
return ranges[range] || 0;
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
/**
|
|
4216
|
-
* Get state category for filtering
|
|
4217
|
-
* @param {string} state - Detailed conversation state
|
|
4218
|
-
* @returns {string} Category: 'active' or 'inactive'
|
|
4219
|
-
*/
|
|
4220
|
-
getStateCategory(state) {
|
|
4221
|
-
// Active states - conversation is currently being used or recently active
|
|
4222
|
-
const activeStates = [
|
|
4223
|
-
'Claude Code working...',
|
|
4224
|
-
'Awaiting user input...',
|
|
4225
|
-
'User typing...',
|
|
4226
|
-
'Awaiting response...',
|
|
4227
|
-
'Recently active'
|
|
4228
|
-
];
|
|
4229
|
-
|
|
4230
|
-
// Inactive states - conversation is idle or old
|
|
4231
|
-
const inactiveStates = [
|
|
4232
|
-
'Idle',
|
|
4233
|
-
'Inactive',
|
|
4234
|
-
'Old',
|
|
4235
|
-
'unknown'
|
|
4236
|
-
];
|
|
4237
|
-
|
|
4238
|
-
if (activeStates.includes(state)) {
|
|
4239
|
-
return 'active';
|
|
4240
|
-
} else if (inactiveStates.includes(state)) {
|
|
4241
|
-
return 'inactive';
|
|
4242
|
-
} else {
|
|
4243
|
-
// Default for any unknown states
|
|
4244
|
-
return 'inactive';
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4247
|
-
|
|
4248
|
-
/**
|
|
4249
|
-
* Get simple conversation preview text (avoids repeating metadata)
|
|
4250
|
-
* @param {Object} conv - Conversation object
|
|
4251
|
-
* @returns {string} Preview text
|
|
4252
|
-
*/
|
|
4253
|
-
getSimpleConversationPreview(conv) {
|
|
4254
|
-
// If we have a last message, show it (this is the most useful info)
|
|
4255
|
-
if (conv.lastMessage && conv.lastMessage.trim()) {
|
|
4256
|
-
const lastMsg = conv.lastMessage.trim();
|
|
4257
|
-
|
|
4258
|
-
// Check if last message is a simple confirmation and try to make it more descriptive
|
|
4259
|
-
if (this.isSimpleConfirmation(lastMsg)) {
|
|
4260
|
-
const messageCount = conv.messageCount || 0;
|
|
4261
|
-
const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'recently';
|
|
4262
|
-
return `User confirmed action • ${messageCount} messages • ${lastActivity}`;
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
|
-
// Check if it's a tool-related message
|
|
4266
|
-
if (lastMsg.includes('Tool Result') || lastMsg.includes('[Tool:')) {
|
|
4267
|
-
return `Tool execution completed • ${this.truncateText(lastMsg, 60)}`;
|
|
4268
|
-
}
|
|
4269
|
-
|
|
4270
|
-
return this.truncateText(lastMsg, 80);
|
|
4271
|
-
}
|
|
4272
|
-
|
|
4273
|
-
// For empty conversations, show descriptive text
|
|
4274
|
-
const messageCount = conv.messageCount || 0;
|
|
4275
|
-
if (messageCount === 0) {
|
|
4276
|
-
return 'Empty conversation - click to start chatting';
|
|
4277
|
-
}
|
|
4278
|
-
|
|
4279
|
-
// For conversations without lastMessage but with messages, show informative text
|
|
4280
|
-
const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'unknown';
|
|
4281
|
-
return `${messageCount} messages • Last activity ${lastActivity}`;
|
|
4282
|
-
}
|
|
4283
|
-
|
|
4284
|
-
/**
|
|
4285
|
-
* Check if a message is a simple confirmation
|
|
4286
|
-
* @param {string} message - Message content
|
|
4287
|
-
* @returns {boolean} True if it's a simple confirmation
|
|
4288
|
-
*/
|
|
4289
|
-
isSimpleConfirmation(message) {
|
|
4290
|
-
const trimmed = message.trim();
|
|
4291
|
-
const confirmationPatterns = [
|
|
4292
|
-
/^\[ok\]$/i,
|
|
4293
|
-
/^ok$/i,
|
|
4294
|
-
/^yes$/i,
|
|
4295
|
-
/^\[yes\]$/i,
|
|
4296
|
-
/^y$/i,
|
|
4297
|
-
/^\[y\]$/i,
|
|
4298
|
-
/^[1-9]$/, // Choice selection
|
|
4299
|
-
/^(continue|proceed|accept|confirm|done)$/i
|
|
4300
|
-
];
|
|
4301
|
-
|
|
4302
|
-
return confirmationPatterns.some(pattern => pattern.test(trimmed));
|
|
4303
|
-
}
|
|
4304
|
-
|
|
4305
|
-
/**
|
|
4306
|
-
* Get conversation preview text (legacy method - still used in other places)
|
|
4307
|
-
* @param {Object} conv - Conversation object
|
|
4308
|
-
* @param {string} state - Conversation state
|
|
4309
|
-
* @returns {string} Preview text
|
|
4310
|
-
*/
|
|
4311
|
-
getConversationPreview(conv, state) {
|
|
4312
|
-
// If we have a last message, show it
|
|
4313
|
-
if (conv.lastMessage && conv.lastMessage.trim()) {
|
|
4314
|
-
return this.truncateText(conv.lastMessage, 60);
|
|
4315
|
-
}
|
|
4316
|
-
|
|
4317
|
-
// Otherwise, show conversation info based on state and metadata
|
|
4318
|
-
const messageCount = conv.messageCount || 0;
|
|
4319
|
-
|
|
4320
|
-
if (messageCount === 0) {
|
|
4321
|
-
return `Empty conversation • Project: ${conv.project || 'Unknown'}`;
|
|
4322
|
-
}
|
|
4323
|
-
|
|
4324
|
-
// Show state-based preview
|
|
4325
|
-
if (state === 'Claude Code working...') {
|
|
4326
|
-
return `Claude is working • ${messageCount} messages`;
|
|
4327
|
-
} else if (state === 'Awaiting user input...') {
|
|
4328
|
-
return `Waiting for your input • ${messageCount} messages`;
|
|
4329
|
-
} else if (state === 'User typing...') {
|
|
4330
|
-
return `Ready for your message • ${messageCount} messages`;
|
|
4331
|
-
} else if (state === 'Recently active') {
|
|
4332
|
-
return `Recently active • ${messageCount} messages`;
|
|
4333
|
-
} else {
|
|
4334
|
-
return `${messageCount} messages • Last active ${this.formatRelativeTime(new Date(conv.lastModified))}`;
|
|
4335
|
-
}
|
|
4336
|
-
}
|
|
4337
|
-
|
|
4338
|
-
/**
|
|
4339
|
-
* Get state CSS class
|
|
4340
|
-
* @param {string} state - Conversation state
|
|
4341
|
-
* @returns {string} CSS class
|
|
4342
|
-
*/
|
|
4343
|
-
getStateClass(state) {
|
|
4344
|
-
const stateClasses = {
|
|
4345
|
-
'Claude Code working...': 'status-active',
|
|
4346
|
-
'Awaiting user input...': 'status-waiting',
|
|
4347
|
-
'User typing...': 'status-typing',
|
|
4348
|
-
'Awaiting response...': 'status-pending',
|
|
4349
|
-
'Recently active': 'status-recent',
|
|
4350
|
-
'Idle': 'status-idle',
|
|
4351
|
-
'Inactive': 'status-inactive',
|
|
4352
|
-
'Old': 'status-old',
|
|
4353
|
-
'unknown': 'status-unknown'
|
|
4354
|
-
};
|
|
4355
|
-
return stateClasses[state] || 'status-unknown';
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4358
|
-
/**
|
|
4359
|
-
* Get state label
|
|
4360
|
-
* @param {string} state - Conversation state
|
|
4361
|
-
* @returns {string} Human readable label
|
|
4362
|
-
*/
|
|
4363
|
-
getStateLabel(state) {
|
|
4364
|
-
const stateLabels = {
|
|
4365
|
-
'Claude Code working...': 'Working',
|
|
4366
|
-
'Awaiting user input...': 'Awaiting input',
|
|
4367
|
-
'User typing...': 'Typing',
|
|
4368
|
-
'Awaiting response...': 'Awaiting response',
|
|
4369
|
-
'Recently active': 'Recent',
|
|
4370
|
-
'Idle': 'Idle',
|
|
4371
|
-
'Inactive': 'Inactive',
|
|
4372
|
-
'Old': 'Old',
|
|
4373
|
-
'unknown': 'Unknown'
|
|
4374
|
-
};
|
|
4375
|
-
return stateLabels[state] || state;
|
|
4376
|
-
}
|
|
4377
|
-
|
|
4378
|
-
/**
|
|
4379
|
-
* Truncate text to specified length
|
|
4380
|
-
* @param {string} text - Text to truncate
|
|
4381
|
-
* @param {number} maxLength - Maximum length
|
|
4382
|
-
* @returns {string} Truncated text
|
|
4383
|
-
*/
|
|
4384
|
-
truncateText(text, maxLength) {
|
|
4385
|
-
if (!text || text.length <= maxLength) return text;
|
|
4386
|
-
return text.substring(0, maxLength - 3) + '...';
|
|
4387
|
-
}
|
|
4388
|
-
|
|
4389
|
-
/**
|
|
4390
|
-
* Update filter
|
|
4391
|
-
* @param {string} filterName - Filter name
|
|
4392
|
-
* @param {string} value - Filter value
|
|
4393
|
-
*/
|
|
4394
|
-
updateFilter(filterName, value) {
|
|
4395
|
-
this.filters[filterName] = value;
|
|
4396
|
-
// When filters change, restart from beginning
|
|
4397
|
-
this.refreshFromBeginning();
|
|
4398
|
-
}
|
|
4399
|
-
|
|
4400
|
-
/**
|
|
4401
|
-
* Clear search
|
|
4402
|
-
*/
|
|
4403
|
-
clearSearch() {
|
|
4404
|
-
const searchInput = this.container.querySelector('#search-filter');
|
|
4405
|
-
if (!searchInput) return; // Guard against null when AgentsPage isn't rendered
|
|
4406
|
-
|
|
4407
|
-
searchInput.value = '';
|
|
4408
|
-
this.updateFilter('search', '');
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
|
-
/**
|
|
4412
|
-
* Clear all filters
|
|
4413
|
-
*/
|
|
4414
|
-
clearAllFilters() {
|
|
4415
|
-
this.filters = {
|
|
4416
|
-
status: 'all',
|
|
4417
|
-
timeRange: '7d',
|
|
4418
|
-
search: ''
|
|
4419
|
-
};
|
|
4420
|
-
|
|
4421
|
-
// Reset UI
|
|
4422
|
-
const statusFilter = this.container.querySelector('#status-filter');
|
|
4423
|
-
const timeFilter = this.container.querySelector('#time-filter');
|
|
4424
|
-
const searchFilter = this.container.querySelector('#search-filter');
|
|
4425
|
-
|
|
4426
|
-
if (statusFilter) statusFilter.value = 'all';
|
|
4427
|
-
if (timeFilter) timeFilter.value = '7d';
|
|
4428
|
-
if (searchFilter) searchFilter.value = '';
|
|
4429
|
-
|
|
4430
|
-
// Restart from beginning when clearing filters
|
|
4431
|
-
this.refreshFromBeginning();
|
|
4432
|
-
}
|
|
4433
|
-
|
|
4434
|
-
/**
|
|
4435
|
-
* Refresh conversations display
|
|
4436
|
-
*/
|
|
4437
|
-
refreshConversationsDisplay() {
|
|
4438
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
4439
|
-
const statesData = this.stateService.getStateProperty('conversationStates') || {};
|
|
4440
|
-
// Extract activeStates from the stored state data
|
|
4441
|
-
const activeStates = statesData?.activeStates || {};
|
|
4442
|
-
this.renderConversationsList(conversations, activeStates);
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
/**
|
|
4446
|
-
* Refresh from beginning - resets pagination
|
|
4447
|
-
*/
|
|
4448
|
-
async refreshFromBeginning() {
|
|
4449
|
-
// Clear cache
|
|
4450
|
-
this.loadedConversations = [];
|
|
4451
|
-
this.loadedMessages.clear();
|
|
4452
|
-
|
|
4453
|
-
// Reset pagination
|
|
4454
|
-
this.pagination = {
|
|
4455
|
-
currentPage: 0,
|
|
4456
|
-
limit: 10,
|
|
4457
|
-
hasMore: true,
|
|
4458
|
-
isLoading: false
|
|
4459
|
-
};
|
|
4460
|
-
|
|
4461
|
-
// Clear list and reload
|
|
4462
|
-
const listContainer = this.container.querySelector('#conversations-list');
|
|
4463
|
-
if (listContainer) {
|
|
4464
|
-
listContainer.innerHTML = '';
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
await this.loadConversationsData();
|
|
4468
|
-
}
|
|
4469
|
-
|
|
4470
|
-
/**
|
|
4471
|
-
* Refresh conversations data
|
|
4472
|
-
*/
|
|
4473
|
-
async refreshConversations() {
|
|
4474
|
-
const refreshBtn = this.container.querySelector('#refresh-conversations');
|
|
4475
|
-
if (!refreshBtn) return; // Guard against null when AgentsPage isn't rendered
|
|
4476
|
-
|
|
4477
|
-
refreshBtn.disabled = true;
|
|
4478
|
-
const iconElement = refreshBtn.querySelector('.btn-icon');
|
|
4479
|
-
if (iconElement) {
|
|
4480
|
-
iconElement.style.animation = 'spin 1s linear infinite';
|
|
4481
|
-
}
|
|
4482
|
-
|
|
4483
|
-
try {
|
|
4484
|
-
// Clear both server and client cache to force fresh data
|
|
4485
|
-
await this.dataService.clearServerCache('conversations');
|
|
4486
|
-
await this.loadConversationsData();
|
|
4487
|
-
} catch (error) {
|
|
4488
|
-
console.error('Error refreshing conversations:', error);
|
|
4489
|
-
this.stateService.setError('Failed to refresh conversations');
|
|
4490
|
-
} finally {
|
|
4491
|
-
refreshBtn.disabled = false;
|
|
4492
|
-
if (iconElement) {
|
|
4493
|
-
iconElement.style.animation = '';
|
|
4494
|
-
}
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
/**
|
|
4499
|
-
* Check if there are active filters
|
|
4500
|
-
* @returns {boolean} True if filters are active
|
|
4501
|
-
*/
|
|
4502
|
-
hasActiveFilters() {
|
|
4503
|
-
const searchInput = this.container.querySelector('#conversation-search');
|
|
4504
|
-
const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
|
4505
|
-
|
|
4506
|
-
// Check if search filter is active
|
|
4507
|
-
if (searchTerm) {
|
|
4508
|
-
return true;
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
|
-
// Check if state filters are active
|
|
4512
|
-
const filterButtons = this.container.querySelectorAll('.filter-btn');
|
|
4513
|
-
const activeFilters = Array.from(filterButtons).filter(btn =>
|
|
4514
|
-
btn.classList.contains('active') && btn.getAttribute('data-state') !== 'all'
|
|
4515
|
-
);
|
|
4516
|
-
|
|
4517
|
-
return activeFilters.length > 0;
|
|
4518
|
-
}
|
|
4519
|
-
|
|
4520
|
-
/**
|
|
4521
|
-
* Update results count
|
|
4522
|
-
* @param {number} count - Number of results
|
|
4523
|
-
* @param {boolean} hasActiveFilters - Whether filters are active
|
|
4524
|
-
*/
|
|
4525
|
-
updateResultsCount(count, hasActiveFilters = false) {
|
|
4526
|
-
// Update main results count
|
|
4527
|
-
const resultsCount = this.container.querySelector('#results-count');
|
|
4528
|
-
if (resultsCount) {
|
|
4529
|
-
let countText = `${count} conversation${count !== 1 ? 's' : ''} found`;
|
|
4530
|
-
if (hasActiveFilters && this.pagination && this.pagination.totalCount && count < this.pagination.totalCount) {
|
|
4531
|
-
countText += ` (filtered from ${this.pagination.totalCount})`;
|
|
4532
|
-
}
|
|
4533
|
-
resultsCount.textContent = countText;
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
|
-
// Update sidebar count
|
|
4537
|
-
const sidebarCount = this.container.querySelector('#sidebar-count');
|
|
4538
|
-
if (sidebarCount) {
|
|
4539
|
-
sidebarCount.textContent = count;
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
|
|
4543
|
-
/**
|
|
4544
|
-
* Show empty state
|
|
4545
|
-
*/
|
|
4546
|
-
showEmptyState() {
|
|
4547
|
-
const conversationsList = this.container.querySelector('#conversations-list');
|
|
4548
|
-
const emptyState = this.container.querySelector('#empty-state');
|
|
4549
|
-
if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
|
|
4550
|
-
|
|
4551
|
-
conversationsList.style.display = 'none';
|
|
4552
|
-
emptyState.style.display = 'flex';
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
|
-
/**
|
|
4556
|
-
* Hide empty state
|
|
4557
|
-
*/
|
|
4558
|
-
hideEmptyState() {
|
|
4559
|
-
const conversationsList = this.container.querySelector('#conversations-list');
|
|
4560
|
-
const emptyState = this.container.querySelector('#empty-state');
|
|
4561
|
-
if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
|
|
4562
|
-
|
|
4563
|
-
conversationsList.style.display = 'block';
|
|
4564
|
-
emptyState.style.display = 'none';
|
|
4565
|
-
}
|
|
4566
|
-
|
|
4567
|
-
/**
|
|
4568
|
-
* Toggle between grid and table view
|
|
4569
|
-
* @param {string} view - View type ('grid' or 'table')
|
|
4570
|
-
*/
|
|
4571
|
-
toggleView(view) {
|
|
4572
|
-
const toggleBtns = this.container.querySelectorAll('.toggle-btn');
|
|
4573
|
-
toggleBtns.forEach(btn => {
|
|
4574
|
-
btn.classList.toggle('active', btn.dataset.view === view);
|
|
4575
|
-
});
|
|
4576
|
-
|
|
4577
|
-
const gridElement = this.container.querySelector('#conversations-grid');
|
|
4578
|
-
const tableSection = this.container.querySelector('.conversations-table-section');
|
|
4579
|
-
|
|
4580
|
-
if (!gridElement || !tableSection) return; // Guard against null when AgentsPage isn't rendered
|
|
4581
|
-
|
|
4582
|
-
const gridSection = gridElement.parentNode;
|
|
4583
|
-
|
|
4584
|
-
if (view === 'table') {
|
|
4585
|
-
gridSection.style.display = 'none';
|
|
4586
|
-
tableSection.style.display = 'block';
|
|
4587
|
-
} else {
|
|
4588
|
-
gridSection.style.display = 'block';
|
|
4589
|
-
tableSection.style.display = 'none';
|
|
4590
|
-
}
|
|
4591
|
-
}
|
|
4592
|
-
|
|
4593
|
-
/**
|
|
4594
|
-
* View conversation details
|
|
4595
|
-
* @param {string} conversationId - Conversation ID
|
|
4596
|
-
*/
|
|
4597
|
-
viewConversation(conversationId) {
|
|
4598
|
-
// This would open a detailed conversation view
|
|
4599
|
-
// Implementation would show conversation details modal or navigate to detail page
|
|
4600
|
-
}
|
|
4601
|
-
|
|
4602
|
-
/**
|
|
4603
|
-
* Export single conversation
|
|
4604
|
-
* @param {string} conversationId - Conversation ID
|
|
4605
|
-
*/
|
|
4606
|
-
exportSingleConversation(conversationId) {
|
|
4607
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
4608
|
-
const conversation = conversations.find(conv => conv.id === conversationId);
|
|
4609
|
-
|
|
4610
|
-
if (conversation) {
|
|
4611
|
-
const dataStr = JSON.stringify(conversation, null, 2);
|
|
4612
|
-
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
4613
|
-
const url = URL.createObjectURL(dataBlob);
|
|
4614
|
-
|
|
4615
|
-
const link = document.createElement('a');
|
|
4616
|
-
link.href = url;
|
|
4617
|
-
link.download = `conversation-${conversationId}-${new Date().toISOString().split('T')[0]}.json`;
|
|
4618
|
-
link.click();
|
|
4619
|
-
|
|
4620
|
-
URL.revokeObjectURL(url);
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
|
|
4624
|
-
/**
|
|
4625
|
-
* Export all conversations
|
|
4626
|
-
*/
|
|
4627
|
-
exportConversations() {
|
|
4628
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
4629
|
-
const states = this.stateService.getStateProperty('conversationStates') || {};
|
|
4630
|
-
const filteredConversations = this.filterConversations(conversations, states);
|
|
4631
|
-
|
|
4632
|
-
const dataStr = JSON.stringify({
|
|
4633
|
-
conversations: filteredConversations,
|
|
4634
|
-
states: states,
|
|
4635
|
-
exportDate: new Date().toISOString(),
|
|
4636
|
-
filters: this.filters
|
|
4637
|
-
}, null, 2);
|
|
4638
|
-
|
|
4639
|
-
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
4640
|
-
const url = URL.createObjectURL(dataBlob);
|
|
4641
|
-
|
|
4642
|
-
const link = document.createElement('a');
|
|
4643
|
-
link.href = url;
|
|
4644
|
-
link.download = `claude-conversations-${new Date().toISOString().split('T')[0]}.json`;
|
|
4645
|
-
link.click();
|
|
4646
|
-
|
|
4647
|
-
URL.revokeObjectURL(url);
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
/**
|
|
4651
|
-
* Update conversations display
|
|
4652
|
-
* @param {Array} conversations - Conversations data
|
|
4653
|
-
*/
|
|
4654
|
-
updateConversationsDisplay(conversations) {
|
|
4655
|
-
const statesData = this.stateService.getStateProperty('conversationStates') || {};
|
|
4656
|
-
const activeStates = statesData?.activeStates || {};
|
|
4657
|
-
this.renderConversationsList(conversations, activeStates);
|
|
4658
|
-
}
|
|
4659
|
-
|
|
4660
|
-
/**
|
|
4661
|
-
* Update conversation states
|
|
4662
|
-
* @param {Object} activeStates - Active conversation states (direct object, not nested)
|
|
4663
|
-
*/
|
|
4664
|
-
updateConversationStates(activeStates) {
|
|
4665
|
-
if (!this.isInitialized) {
|
|
4666
|
-
console.warn('AgentsPage: updateConversationStates called before initialization');
|
|
4667
|
-
return;
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
|
-
// Check if we're still on the agents page by verifying our key element exists
|
|
4671
|
-
const conversationsContainer = this.container.querySelector('#conversations-list');
|
|
4672
|
-
if (!conversationsContainer) {
|
|
4673
|
-
console.log('AgentsPage: Not on agents page, skipping conversation states update');
|
|
4674
|
-
return;
|
|
4675
|
-
}
|
|
4676
|
-
|
|
4677
|
-
const conversations = this.stateService.getStateProperty('conversations') || [];
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
// Re-render conversation list with new states
|
|
4681
|
-
this.renderConversationsList(conversations, activeStates || {});
|
|
4682
|
-
|
|
4683
|
-
// Update banner if we have a selected conversation
|
|
4684
|
-
if (this.selectedConversationId && activeStates && activeStates[this.selectedConversationId]) {
|
|
4685
|
-
this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
|
|
4689
|
-
/**
|
|
4690
|
-
* Handle conversation state change
|
|
4691
|
-
* @param {Object} _state - New state (unused but required by interface)
|
|
4692
|
-
*/
|
|
4693
|
-
handleConversationStateChange(_state) {
|
|
4694
|
-
this.refreshConversationsDisplay();
|
|
4695
|
-
}
|
|
4696
|
-
|
|
4697
|
-
/**
|
|
4698
|
-
* Update loading state
|
|
4699
|
-
* @param {boolean} isLoading - Loading state
|
|
4700
|
-
*/
|
|
4701
|
-
updateLoadingState(isLoading) {
|
|
4702
|
-
const loadingState = this.container.querySelector('#conversations-loading');
|
|
4703
|
-
if (loadingState) {
|
|
4704
|
-
loadingState.style.display = isLoading ? 'flex' : 'none';
|
|
4705
|
-
}
|
|
4706
|
-
}
|
|
4707
|
-
|
|
4708
|
-
/**
|
|
4709
|
-
* Update error state
|
|
4710
|
-
* @param {Error|string} error - Error object or message
|
|
4711
|
-
*/
|
|
4712
|
-
updateErrorState(error) {
|
|
4713
|
-
const errorState = this.container.querySelector('#conversations-error');
|
|
4714
|
-
const errorMessage = this.container.querySelector('.error-message');
|
|
4715
|
-
|
|
4716
|
-
if (errorState && errorMessage) {
|
|
4717
|
-
if (error) {
|
|
4718
|
-
errorMessage.textContent = error.message || error;
|
|
4719
|
-
errorState.style.display = 'flex';
|
|
4720
|
-
} else {
|
|
4721
|
-
errorState.style.display = 'none';
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
/**
|
|
4727
|
-
* Destroy agents page
|
|
4728
|
-
*/
|
|
4729
|
-
destroy() {
|
|
4730
|
-
// Cleanup header component
|
|
4731
|
-
if (this.headerComponent) {
|
|
4732
|
-
this.headerComponent.destroy();
|
|
4733
|
-
this.headerComponent = null;
|
|
4734
|
-
}
|
|
4735
|
-
|
|
4736
|
-
// Cleanup components
|
|
4737
|
-
Object.values(this.components).forEach(component => {
|
|
4738
|
-
if (component.destroy) {
|
|
4739
|
-
component.destroy();
|
|
4740
|
-
}
|
|
4741
|
-
});
|
|
4742
|
-
|
|
4743
|
-
// Cleanup scroll listeners
|
|
4744
|
-
const messagesContent = this.container.querySelector('#messages-content');
|
|
4745
|
-
if (messagesContent && this.messagesScrollListener) {
|
|
4746
|
-
messagesContent.removeEventListener('scroll', this.messagesScrollListener);
|
|
4747
|
-
}
|
|
4748
|
-
|
|
4749
|
-
// Unsubscribe from state changes
|
|
4750
|
-
if (this.unsubscribe) {
|
|
4751
|
-
this.unsubscribe();
|
|
4752
|
-
}
|
|
4753
|
-
|
|
4754
|
-
this.isInitialized = false;
|
|
4755
|
-
}
|
|
4756
|
-
}
|
|
4757
|
-
|
|
4758
|
-
// Export for module use
|
|
4759
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
4760
|
-
module.exports = AgentsPage;
|
|
4761
|
-
}
|