vibesurf 0.1.9a6__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/vibe_surf_agent.py +25 -15
  3. vibe_surf/backend/api/browser.py +66 -0
  4. vibe_surf/backend/api/task.py +2 -1
  5. vibe_surf/backend/main.py +76 -1
  6. vibe_surf/backend/shared_state.py +2 -0
  7. vibe_surf/browser/agent_browser_session.py +312 -62
  8. vibe_surf/browser/browser_manager.py +57 -92
  9. vibe_surf/browser/watchdogs/dom_watchdog.py +43 -43
  10. vibe_surf/chrome_extension/background.js +84 -0
  11. vibe_surf/chrome_extension/manifest.json +3 -1
  12. vibe_surf/chrome_extension/scripts/file-manager.js +526 -0
  13. vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
  14. vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
  15. vibe_surf/chrome_extension/scripts/session-manager.js +31 -8
  16. vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
  17. vibe_surf/chrome_extension/scripts/ui-manager.js +770 -3186
  18. vibe_surf/chrome_extension/sidepanel.html +27 -4
  19. vibe_surf/chrome_extension/styles/activity.css +574 -0
  20. vibe_surf/chrome_extension/styles/base.css +76 -0
  21. vibe_surf/chrome_extension/styles/history-modal.css +791 -0
  22. vibe_surf/chrome_extension/styles/input.css +429 -0
  23. vibe_surf/chrome_extension/styles/layout.css +186 -0
  24. vibe_surf/chrome_extension/styles/responsive.css +454 -0
  25. vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
  26. vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
  27. vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
  28. vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
  29. vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
  30. vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
  31. vibe_surf/chrome_extension/styles/variables.css +54 -0
  32. vibe_surf/cli.py +1 -0
  33. vibe_surf/controller/vibesurf_tools.py +0 -2
  34. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/METADATA +18 -2
  35. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/RECORD +39 -23
  36. vibe_surf/chrome_extension/styles/main.css +0 -2338
  37. vibe_surf/chrome_extension/styles/settings.css +0 -1100
  38. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/WHEEL +0 -0
  39. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/entry_points.txt +0 -0
  40. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/licenses/LICENSE +0 -0
  41. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
- // UI Manager - Handles all user interface interactions and state updates
2
- // Manages DOM manipulation, form handling, modal displays, and UI state
1
+ // UI Manager - Core UI management and session interaction
2
+ // Coordinates with specialized modules for settings, history, files, and modals
3
3
 
4
4
  class VibeSurfUIManager {
5
5
  constructor(sessionManager, apiClient) {
@@ -8,30 +8,20 @@ class VibeSurfUIManager {
8
8
  this.elements = {};
9
9
  this.state = {
10
10
  isLoading: false,
11
- currentModal: null,
12
- llmProfiles: [],
13
- mcpProfiles: [],
14
- settings: {},
15
11
  isTaskRunning: false,
16
- taskInfo: null,
17
- // File upload state
18
- uploadedFiles: [],
19
- // History-related state
20
- historyMode: 'recent', // 'recent' or 'all'
21
- currentPage: 1,
22
- totalPages: 1,
23
- pageSize: 10,
24
- searchQuery: '',
25
- statusFilter: 'all',
26
- recentTasks: [],
27
- allSessions: []
12
+ taskInfo: null
28
13
  };
29
14
 
15
+ // Initialize specialized managers
16
+ this.settingsManager = null;
17
+ this.historyManager = null;
18
+ this.fileManager = null;
19
+ this.modalManager = null;
20
+
30
21
  this.bindElements();
22
+ this.initializeManagers();
31
23
  this.bindEvents();
32
24
  this.setupSessionListeners();
33
- this.setupHistoryModalHandlers();
34
-
35
25
  }
36
26
 
37
27
  bindElements() {
@@ -58,46 +48,7 @@ class VibeSurfUIManager {
58
48
  // Input area
59
49
  llmProfileSelect: document.getElementById('llm-profile-select'),
60
50
  taskInput: document.getElementById('task-input'),
61
- attachFileBtn: document.getElementById('attach-file-btn'),
62
51
  sendBtn: document.getElementById('send-btn'),
63
- fileInput: document.getElementById('file-input'),
64
-
65
- // Modals
66
- historyModal: document.getElementById('history-modal'),
67
- settingsModal: document.getElementById('settings-modal'),
68
-
69
- // History Modal Elements
70
- recentTasksList: document.getElementById('recent-tasks-list'),
71
- viewMoreTasksBtn: document.getElementById('view-more-tasks-btn'),
72
- allSessionsSection: document.getElementById('all-sessions-section'),
73
- backToRecentBtn: document.getElementById('back-to-recent-btn'),
74
- sessionSearch: document.getElementById('session-search'),
75
- sessionFilter: document.getElementById('session-filter'),
76
- allSessionsList: document.getElementById('all-sessions-list'),
77
- prevPageBtn: document.getElementById('prev-page-btn'),
78
- nextPageBtn: document.getElementById('next-page-btn'),
79
- pageInfo: document.getElementById('page-info'),
80
-
81
- // Settings - New Structure
82
- settingsTabs: document.querySelectorAll('.settings-tab'),
83
- settingsTabContents: document.querySelectorAll('.settings-tab-content'),
84
- llmProfilesContainer: document.getElementById('llm-profiles-container'),
85
- mcpProfilesContainer: document.getElementById('mcp-profiles-container'),
86
- addLlmProfileBtn: document.getElementById('add-llm-profile-btn'),
87
- addMcpProfileBtn: document.getElementById('add-mcp-profile-btn'),
88
- backendUrl: document.getElementById('backend-url'),
89
-
90
- // Profile Form Modal
91
- profileFormModal: document.getElementById('profile-form-modal'),
92
- profileFormTitle: document.getElementById('profile-form-title'),
93
- profileForm: document.getElementById('profile-form'),
94
- profileFormCancel: document.getElementById('profile-form-cancel'),
95
- profileFormSubmit: document.getElementById('profile-form-submit'),
96
- profileFormClose: document.querySelector('.profile-form-close'),
97
-
98
- // Environment Variables
99
- envVariablesList: document.getElementById('env-variables-list'),
100
- saveEnvVarsBtn: document.getElementById('save-env-vars-btn'),
101
52
 
102
53
  // Loading
103
54
  loadingOverlay: document.getElementById('loading-overlay')
@@ -112,6 +63,147 @@ class VibeSurfUIManager {
112
63
  }
113
64
  }
114
65
 
66
+ initializeManagers() {
67
+ try {
68
+ // Initialize modal manager first (others may depend on it)
69
+ this.modalManager = new VibeSurfModalManager();
70
+
71
+ // Initialize other managers
72
+ this.settingsManager = new VibeSurfSettingsManager(this.apiClient);
73
+ this.historyManager = new VibeSurfHistoryManager(this.apiClient);
74
+ this.fileManager = new VibeSurfFileManager(this.sessionManager);
75
+
76
+ // Set up inter-manager communication
77
+ this.setupManagerEvents();
78
+
79
+ console.log('[UIManager] All specialized managers initialized');
80
+ } catch (error) {
81
+ console.error('[UIManager] Failed to initialize managers:', error);
82
+ this.showNotification('Failed to initialize UI components', 'error');
83
+ }
84
+ }
85
+
86
+ setupManagerEvents() {
87
+ // Settings Manager Events
88
+ this.settingsManager.on('profilesUpdated', () => {
89
+ this.updateLLMProfileSelect();
90
+ });
91
+
92
+ this.settingsManager.on('notification', (data) => {
93
+ this.showNotification(data.message, data.type);
94
+ });
95
+
96
+ this.settingsManager.on('loading', (data) => {
97
+ if (data.hide) {
98
+ this.hideLoading();
99
+ } else {
100
+ this.showLoading(data.message);
101
+ }
102
+ });
103
+
104
+ this.settingsManager.on('error', (data) => {
105
+ console.error('[UIManager] Settings error:', data.error);
106
+ this.showNotification(data.message || 'Settings error occurred', 'error');
107
+ });
108
+
109
+ this.settingsManager.on('confirmDeletion', (data) => {
110
+ this.modalManager.showConfirmModal(
111
+ 'Delete Profile',
112
+ `Are you sure you want to delete the ${data.type} profile "${data.profileId}"? This action cannot be undone.`,
113
+ {
114
+ confirmText: 'Delete',
115
+ cancelText: 'Cancel',
116
+ type: 'danger',
117
+ onConfirm: () => {
118
+ if (data.callback) data.callback();
119
+ }
120
+ }
121
+ );
122
+ });
123
+
124
+ this.settingsManager.on('selectNewDefault', (data) => {
125
+ const options = data.otherProfiles.map(profile =>
126
+ `<option value="${profile.profile_name}">${profile.profile_name}</option>`
127
+ ).join('');
128
+
129
+ const modalData = this.modalManager.createModal(`
130
+ <div class="select-default-modal">
131
+ <p>You are deleting the default LLM profile. Please select a new default profile:</p>
132
+ <select id="new-default-select" class="form-select">
133
+ ${options}
134
+ </select>
135
+ <div class="modal-footer">
136
+ <button class="btn-secondary cancel-btn">Cancel</button>
137
+ <button class="btn-primary confirm-btn">Set as Default & Delete</button>
138
+ </div>
139
+ </div>
140
+ `, {
141
+ title: 'Select New Default Profile'
142
+ });
143
+
144
+ const select = document.getElementById('new-default-select');
145
+ const confirmBtn = modalData.modal.querySelector('.confirm-btn');
146
+ const cancelBtn = modalData.modal.querySelector('.cancel-btn');
147
+
148
+ confirmBtn.addEventListener('click', () => {
149
+ const newDefaultId = select.value;
150
+ if (newDefaultId && data.callback) {
151
+ data.callback(newDefaultId);
152
+ }
153
+ modalData.close();
154
+ });
155
+
156
+ cancelBtn.addEventListener('click', () => {
157
+ modalData.close();
158
+ });
159
+ });
160
+
161
+ // History Manager Events
162
+ this.historyManager.on('loadSession', (data) => {
163
+ this.loadSession(data.sessionId);
164
+ });
165
+
166
+ this.historyManager.on('loading', (data) => {
167
+ if (data.hide) {
168
+ this.hideLoading();
169
+ } else {
170
+ this.showLoading(data.message);
171
+ }
172
+ });
173
+
174
+ this.historyManager.on('notification', (data) => {
175
+ this.showNotification(data.message, data.type);
176
+ });
177
+
178
+ this.historyManager.on('error', (data) => {
179
+ console.error('[UIManager] History error:', data.error);
180
+ this.showNotification(data.message || 'History error occurred', 'error');
181
+ });
182
+
183
+ // File Manager Events
184
+ this.fileManager.on('loading', (data) => {
185
+ if (data.hide) {
186
+ this.hideLoading();
187
+ } else {
188
+ this.showLoading(data.message);
189
+ }
190
+ });
191
+
192
+ this.fileManager.on('notification', (data) => {
193
+ this.showNotification(data.message, data.type);
194
+ });
195
+
196
+ this.fileManager.on('error', (data) => {
197
+ console.error('[UIManager] File error:', data.error);
198
+ this.showNotification(data.message || 'File error occurred', 'error');
199
+ });
200
+
201
+ // Modal Manager Events (if needed for future extensions)
202
+ this.modalManager.on('modalClosed', (data) => {
203
+ // Handle modal close events if needed
204
+ });
205
+ }
206
+
115
207
  bindEvents() {
116
208
  // Header buttons
117
209
  this.elements.newSessionBtn?.addEventListener('click', this.handleNewSession.bind(this));
@@ -128,8 +220,6 @@ class VibeSurfUIManager {
128
220
 
129
221
  // Input handling
130
222
  this.elements.sendBtn?.addEventListener('click', this.handleSendTask.bind(this));
131
- this.elements.attachFileBtn?.addEventListener('click', this.handleAttachFiles.bind(this));
132
- this.elements.fileInput?.addEventListener('change', this.handleFileSelection.bind(this));
133
223
 
134
224
  // Task input handling
135
225
  this.elements.taskInput?.addEventListener('keydown', this.handleTaskInputKeydown.bind(this));
@@ -147,288 +237,6 @@ class VibeSurfUIManager {
147
237
 
148
238
  // Bind initial task suggestions if present
149
239
  this.bindTaskSuggestionEvents();
150
-
151
- // Settings handling
152
- this.elements.backendUrl?.addEventListener('change', this.handleBackendUrlChange.bind(this));
153
- this.elements.addLlmProfileBtn?.addEventListener('click', () => this.handleAddProfile('llm'));
154
- this.elements.addMcpProfileBtn?.addEventListener('click', () => this.handleAddProfile('mcp'));
155
-
156
- // Settings tabs
157
- this.elements.settingsTabs?.forEach(tab => {
158
- tab.addEventListener('click', this.handleTabSwitch.bind(this));
159
- });
160
-
161
- // Profile form modal
162
- this.elements.profileFormCancel?.addEventListener('click', this.closeProfileForm.bind(this));
163
- this.elements.profileFormClose?.addEventListener('click', this.closeProfileForm.bind(this));
164
-
165
- // Profile form submission - add both form submit and button click handlers
166
- if (this.elements.profileForm) {
167
- this.elements.profileForm.addEventListener('submit', this.handleProfileFormSubmit.bind(this));
168
- console.log('[UIManager] Profile form submit listener added');
169
- } else {
170
- console.warn('[UIManager] Profile form element not found during initialization');
171
- }
172
-
173
- if (this.elements.profileFormSubmit) {
174
- this.elements.profileFormSubmit.addEventListener('click', this.handleProfileFormSubmitClick.bind(this));
175
- console.log('[UIManager] Profile form submit button listener added');
176
- } else {
177
- console.warn('[UIManager] Profile form submit button not found during initialization');
178
- }
179
-
180
- // Environment variables
181
- this.elements.saveEnvVarsBtn?.addEventListener('click', this.handleSaveEnvironmentVariables.bind(this));
182
-
183
- // Modal handling
184
- this.bindModalEvents();
185
-
186
- // File link handling
187
- this.bindFileLinkEvents();
188
-
189
- }
190
-
191
- bindModalEvents() {
192
- // Close modals when clicking overlay or close button
193
- document.addEventListener('click', (event) => {
194
- if (event.target.classList.contains('modal-overlay') ||
195
- event.target.classList.contains('modal-close') ||
196
- event.target.closest('.modal-close')) {
197
- this.closeModal();
198
- }
199
- });
200
-
201
- // Close modals with Escape key
202
- document.addEventListener('keydown', (event) => {
203
- if (event.key === 'Escape' && this.state.currentModal) {
204
- this.closeModal();
205
- }
206
- });
207
- }
208
-
209
- bindFileLinkEvents() {
210
- // Handle file:// link clicks with delegation
211
- document.addEventListener('click', (event) => {
212
- const target = event.target;
213
-
214
- // Check if clicked element is a file link
215
- if (target.matches('a.file-link') || target.closest('a.file-link')) {
216
- event.preventDefault();
217
-
218
- const fileLink = target.matches('a.file-link') ? target : target.closest('a.file-link');
219
- const filePath = fileLink.getAttribute('data-file-path');
220
-
221
- this.handleFileLink(filePath);
222
- }
223
- });
224
-
225
- }
226
-
227
- async handleFileLink(filePath) {
228
- // Prevent multiple simultaneous calls
229
- if (this._isHandlingFileLink) {
230
- return;
231
- }
232
-
233
- this._isHandlingFileLink = true;
234
-
235
- try {
236
- // First decode the URL-encoded path
237
- let decodedPath = decodeURIComponent(filePath);
238
-
239
- // Remove file:// protocol prefix and normalize
240
- let cleanPath = decodedPath.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
241
-
242
- // Ensure path starts with / for Unix paths if not Windows drive
243
- if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
244
- cleanPath = '/' + cleanPath;
245
- }
246
-
247
- // Convert all backslashes to forward slashes
248
- cleanPath = cleanPath.replace(/\\/g, '/');
249
-
250
- // Create proper file URL - always use triple slash for proper format
251
- const fileUrl = cleanPath.match(/^[A-Za-z]:/) ?
252
- `file:///${cleanPath}` :
253
- `file:///${cleanPath.replace(/^\//, '')}`; // Remove leading slash and add triple slash
254
-
255
- // Create Windows format path for system open
256
- const windowsPath = cleanPath.replace(/\//g, '\\');
257
-
258
- // Show user notification about the action
259
- this.showNotification(`Opening file: ${cleanPath}`, 'info');
260
-
261
- // Use setTimeout to prevent UI blocking
262
- setTimeout(async () => {
263
- try {
264
- // Primary strategy: Try browser open first for HTML files (more reliable)
265
- if (fileUrl.toLowerCase().endsWith('.html') || fileUrl.toLowerCase().endsWith('.htm')) {
266
- try {
267
- const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
268
- if (opened) {
269
- this.showNotification('File opened in browser', 'success');
270
- return;
271
- } else {
272
- // If browser is blocked, try system open
273
- await this.trySystemOpen(windowsPath, fileUrl);
274
- return;
275
- }
276
- } catch (browserError) {
277
- await this.trySystemOpen(windowsPath, fileUrl);
278
- return;
279
- }
280
- } else {
281
- // For non-HTML files, try system open first
282
- const systemSuccess = await this.trySystemOpen(windowsPath, fileUrl);
283
- if (systemSuccess) return;
284
-
285
- // Fallback to browser if system open fails
286
- try {
287
- const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
288
- if (opened) {
289
- this.showNotification('File opened in browser', 'success');
290
- return;
291
- }
292
- } catch (browserError) {
293
- console.error('[UIManager] Failed to open file:', browserError);
294
- }
295
- }
296
-
297
- // Last resort: Copy path to clipboard
298
- this.copyToClipboardFallback(fileUrl);
299
-
300
- } catch (error) {
301
- console.error('[UIManager] Error in async file handling:', error);
302
- this.showNotification(`Unable to open file: ${error.message}`, 'error');
303
- } finally {
304
- this._isHandlingFileLink = false;
305
- }
306
- }, 50); // Small delay to prevent UI blocking
307
-
308
- } catch (error) {
309
- console.error('[UIManager] Error handling file link:', error);
310
- this.showNotification(`Unable to open file: ${error.message}`, 'error');
311
- this._isHandlingFileLink = false;
312
- }
313
- }
314
-
315
- async trySystemOpen(windowsPath, fileUrl) {
316
- try {
317
- const systemOpenPromise = chrome.runtime.sendMessage({
318
- type: 'OPEN_FILE_SYSTEM',
319
- data: { filePath: windowsPath }
320
- });
321
-
322
- // Add timeout to prevent hanging
323
- const systemOpenResponse = await Promise.race([
324
- systemOpenPromise,
325
- new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
326
- ]);
327
-
328
- if (systemOpenResponse && systemOpenResponse.success) {
329
- this.showNotification('File opened with system default application', 'success');
330
- return true;
331
- }
332
- return false;
333
- } catch (systemError) {
334
- return false;
335
- }
336
- }
337
-
338
- async copyToClipboardFallback(fileUrl) {
339
- try {
340
- await navigator.clipboard.writeText(fileUrl);
341
- this.showNotification('File URL copied to clipboard - paste in browser address bar', 'info');
342
- } catch (clipboardError) {
343
- console.error('[UIManager] Clipboard failed:', clipboardError);
344
- this.showNotification('Unable to open file. URL: ' + fileUrl, 'warning');
345
- }
346
- }
347
-
348
- showFileAccessInstructions(windowsPath, fileUrl, unixPath) {
349
- const modal = this.createWarningModal({
350
- title: 'File Access Options',
351
- message: 'Chrome extensions cannot directly open local files due to security restrictions. Choose one of these methods to access your file:',
352
- details: `Windows Path: ${windowsPath}\nFile URL: ${fileUrl}\n\nRecommended Methods:\n1. Copy path and open in File Explorer\n2. Copy URL and paste in new browser tab\n3. Use "Open with" from File Explorer`,
353
- buttons: [
354
- {
355
- text: 'Copy Windows Path',
356
- style: 'primary',
357
- action: async () => {
358
- try {
359
- await navigator.clipboard.writeText(windowsPath);
360
- this.showNotification('Windows file path copied to clipboard', 'success');
361
- } catch (error) {
362
- console.error('Failed to copy path:', error);
363
- this.showNotification('Failed to copy, please copy manually', 'error');
364
- }
365
- this.closeWarningModal();
366
- }
367
- },
368
- {
369
- text: 'Copy File URL',
370
- style: 'secondary',
371
- action: async () => {
372
- try {
373
- await navigator.clipboard.writeText(fileUrl);
374
- this.showNotification('File URL copied to clipboard', 'success');
375
- } catch (error) {
376
- console.error('Failed to copy URL:', error);
377
- this.showNotification('Failed to copy, please copy manually', 'error');
378
- }
379
- this.closeWarningModal();
380
- }
381
- },
382
- {
383
- text: 'Try Open URL',
384
- style: 'secondary',
385
- action: async () => {
386
- try {
387
-
388
- // Try via background script first
389
- const response = await chrome.runtime.sendMessage({
390
- type: 'OPEN_FILE_URL',
391
- data: { fileUrl: fileUrl }
392
- });
393
-
394
-
395
- if (response && response.success) {
396
- this.showNotification('File opened successfully', 'success');
397
- this.closeWarningModal();
398
- return;
399
- }
400
-
401
-
402
- // Fallback to window.open
403
- const opened = window.open(fileUrl, '_blank');
404
-
405
- if (!opened) {
406
- this.showNotification('Popup blocked. Try copying URL instead.', 'warning');
407
- } else {
408
- this.showNotification('Attempting to open file in new tab...', 'info');
409
-
410
- // Check if the tab actually loaded the file after a delay
411
- setTimeout(() => {
412
- }, 2000);
413
- }
414
- } catch (error) {
415
- console.error('[UIManager] Failed to open file:', error);
416
- this.showNotification('Failed to open. Use copy options instead.', 'error');
417
- }
418
- this.closeWarningModal();
419
- }
420
- },
421
- {
422
- text: 'Close',
423
- style: 'secondary',
424
- action: () => {
425
- this.closeWarningModal();
426
- }
427
- }
428
- ]
429
- });
430
-
431
- document.body.appendChild(modal);
432
240
  }
433
241
 
434
242
  setupSessionListeners() {
@@ -459,7 +267,6 @@ class VibeSurfUIManager {
459
267
  }
460
268
 
461
269
  handleSessionLoaded(data) {
462
-
463
270
  // Update session display
464
271
  this.updateSessionDisplay(data.sessionId);
465
272
 
@@ -472,7 +279,6 @@ class VibeSurfUIManager {
472
279
  if (taskStatus) {
473
280
  this.updateControlPanel(taskStatus);
474
281
  }
475
-
476
282
  }
477
283
 
478
284
  handleTaskSubmitted(data) {
@@ -505,8 +311,7 @@ class VibeSurfUIManager {
505
311
  // Check if we need to respect minimum visibility period
506
312
  if (this.controlPanelMinVisibilityActive) {
507
313
  console.log('[UIManager] Task completed during minimum visibility period, delaying control panel hide');
508
- // Wait for minimum visibility period to end before hiding
509
- const remainingTime = 1000; // Could be calculated more precisely, but 2s is reasonable
314
+ const remainingTime = 1000;
510
315
  setTimeout(() => {
511
316
  console.log('[UIManager] Minimum visibility period respected, now hiding control panel');
512
317
  this.updateControlPanel('ready');
@@ -519,7 +324,7 @@ class VibeSurfUIManager {
519
324
  this.showNotification(message, type);
520
325
 
521
326
  // Clear uploaded files when task is completed
522
- this.clearUploadedFiles();
327
+ this.fileManager.clearUploadedFiles();
523
328
  }
524
329
 
525
330
  handleNewActivity(data) {
@@ -544,8 +349,6 @@ class VibeSurfUIManager {
544
349
  console.error('[UIManager] Task error:', data.error);
545
350
  this.showNotification(`Task error: ${data.error}`, 'error');
546
351
 
547
- // ✅ FIXED: Keep control panel visible during errors
548
- // Don't assume task stopped - it might still be running server-side
549
352
  this.updateControlPanel('error');
550
353
 
551
354
  // Optional: Verify task status after a delay
@@ -559,7 +362,6 @@ class VibeSurfUIManager {
559
362
  }
560
363
  }).catch(err => {
561
364
  console.warn('[UIManager] Could not verify task status after error:', err);
562
- // If we can't verify, keep controls visible for safety
563
365
  });
564
366
  }, 1000);
565
367
  }
@@ -586,7 +388,7 @@ class VibeSurfUIManager {
586
388
  }
587
389
 
588
390
  startTaskStatusMonitoring() {
589
- // Check task status every 2 seconds
391
+ // Check task status every 500ms
590
392
  this.taskStatusInterval = setInterval(() => {
591
393
  this.checkTaskStatus();
592
394
  }, 500);
@@ -616,46 +418,28 @@ class VibeSurfUIManager {
616
418
  this.elements.llmProfileSelect.disabled = isRunning;
617
419
  }
618
420
 
619
- if (this.elements.attachFileBtn) {
620
- this.elements.attachFileBtn.disabled = isRunning;
621
- }
421
+ // Update file manager state
422
+ this.fileManager.setEnabled(!isRunning);
622
423
 
623
424
  // Also disable header buttons when task is running
624
- if (this.elements.newSessionBtn) {
625
- this.elements.newSessionBtn.disabled = isRunning;
626
- }
627
-
628
- if (this.elements.historyBtn) {
629
- this.elements.historyBtn.disabled = isRunning;
630
- }
631
-
632
- if (this.elements.settingsBtn) {
633
- this.elements.settingsBtn.disabled = isRunning;
634
- }
635
-
636
- // Add visual feedback to indicate locked state
637
- const lockableElements = [
638
- this.elements.taskInput,
639
- this.elements.sendBtn,
640
- this.elements.llmProfileSelect,
641
- this.elements.attachFileBtn,
425
+ const headerButtons = [
642
426
  this.elements.newSessionBtn,
643
427
  this.elements.historyBtn,
644
428
  this.elements.settingsBtn
645
429
  ];
646
430
 
647
- lockableElements.forEach(element => {
648
- if (element) {
431
+ headerButtons.forEach(button => {
432
+ if (button) {
433
+ button.disabled = isRunning;
649
434
  if (isRunning) {
650
- element.classList.add('task-running-disabled');
651
- element.setAttribute('title', 'Disabled while task is running');
435
+ button.classList.add('task-running-disabled');
436
+ button.setAttribute('title', 'Disabled while task is running');
652
437
  } else {
653
- element.classList.remove('task-running-disabled');
654
- element.removeAttribute('title');
438
+ button.classList.remove('task-running-disabled');
439
+ button.removeAttribute('title');
655
440
  }
656
441
  }
657
442
  });
658
-
659
443
  }
660
444
 
661
445
  canSubmitTask() {
@@ -670,76 +454,24 @@ class VibeSurfUIManager {
670
454
  const taskId = taskInfo?.task_id || 'unknown';
671
455
  const sessionId = taskInfo?.session_id || 'unknown';
672
456
 
673
- return new Promise((resolve) => {
674
- const modal = this.createWarningModal({
675
- title: 'Task Currently Running',
676
- message: `A task is currently ${taskInfo?.status || 'running'}. You must stop the current task before you can ${action}.`,
677
- details: `Task ID: ${taskId}\nSession ID: ${sessionId}`,
678
- buttons: [
679
- {
680
- text: 'Stop Current Task',
681
- style: 'danger',
682
- action: async () => {
683
- try {
684
- await this.sessionManager.stopTask('User wants to perform new action');
685
- this.closeWarningModal();
686
- resolve(true);
687
- } catch (error) {
688
- this.showNotification(`Failed to stop task: ${error.message}`, 'error');
689
- resolve(false);
690
- }
691
- }
692
- },
693
- {
694
- text: 'Cancel',
695
- style: 'secondary',
696
- action: () => {
697
- this.closeWarningModal();
698
- resolve(false);
699
- }
457
+ return this.modalManager.showConfirmModalAsync(
458
+ 'Task Currently Running',
459
+ `A task is currently ${taskInfo?.status || 'running'}. You must stop the current task before you can ${action}.`,
460
+ {
461
+ confirmText: 'Stop Current Task',
462
+ cancelText: 'Cancel',
463
+ type: 'danger',
464
+ onConfirm: async () => {
465
+ try {
466
+ await this.sessionManager.stopTask('User wants to perform new action');
467
+ return true;
468
+ } catch (error) {
469
+ this.showNotification(`Failed to stop task: ${error.message}`, 'error');
470
+ return false;
700
471
  }
701
- ]
702
- });
703
-
704
- document.body.appendChild(modal);
705
- });
706
- }
707
-
708
- createWarningModal({ title, message, details, buttons }) {
709
- const modal = document.createElement('div');
710
- modal.className = 'modal-overlay warning-modal';
711
- modal.innerHTML = `
712
- <div class="modal-content warning-content">
713
- <div class="warning-header">
714
- <div class="warning-icon">⚠️</div>
715
- <h3>${title}</h3>
716
- </div>
717
- <div class="warning-body">
718
- <p>${message}</p>
719
- ${details ? `<pre class="warning-details">${details}</pre>` : ''}
720
- </div>
721
- <div class="warning-actions">
722
- ${buttons.map((btn, index) =>
723
- `<button class="btn btn-${btn.style}" data-action="${index}">${btn.text}</button>`
724
- ).join('')}
725
- </div>
726
- </div>
727
- `;
728
-
729
- // Add click handlers
730
- buttons.forEach((btn, index) => {
731
- const btnElement = modal.querySelector(`[data-action="${index}"]`);
732
- btnElement.addEventListener('click', btn.action);
733
- });
734
-
735
- return modal;
736
- }
737
-
738
- closeWarningModal() {
739
- const modal = document.querySelector('.warning-modal');
740
- if (modal) {
741
- modal.remove();
742
- }
472
+ }
473
+ }
474
+ );
743
475
  }
744
476
 
745
477
  // UI Action Handlers
@@ -771,19 +503,7 @@ class VibeSurfUIManager {
771
503
  if (!canProceed) return;
772
504
  }
773
505
 
774
- try {
775
- this.showLoading('Loading recent tasks...');
776
-
777
- // Reset to recent tasks view
778
- this.state.historyMode = 'recent';
779
- await this.loadRecentTasks();
780
- this.displayHistoryModal();
781
-
782
- this.hideLoading();
783
- } catch (error) {
784
- this.hideLoading();
785
- this.showNotification(`Failed to load history: ${error.message}`, 'error');
786
- }
506
+ this.historyManager.showHistory();
787
507
  }
788
508
 
789
509
  async handleShowSettings() {
@@ -794,17 +514,7 @@ class VibeSurfUIManager {
794
514
  if (!canProceed) return;
795
515
  }
796
516
 
797
- try {
798
- this.showLoading('Loading settings...');
799
-
800
- await this.loadSettingsData();
801
- this.displaySettingsModal();
802
-
803
- this.hideLoading();
804
- } catch (error) {
805
- this.hideLoading();
806
- this.showNotification(`Failed to load settings: ${error.message}`, 'error');
807
- }
517
+ this.settingsManager.showSettings();
808
518
  }
809
519
 
810
520
  async handleCopySession() {
@@ -847,7 +557,8 @@ class VibeSurfUIManager {
847
557
  // Check if LLM profile is selected
848
558
  if (!llmProfile || llmProfile.trim() === '') {
849
559
  // Check if there are any LLM profiles available
850
- if (this.state.llmProfiles.length === 0) {
560
+ const profiles = this.settingsManager.getLLMProfiles();
561
+ if (profiles.length === 0) {
851
562
  // No LLM profiles configured at all
852
563
  this.showLLMProfileRequiredModal('configure');
853
564
  } else {
@@ -858,50 +569,26 @@ class VibeSurfUIManager {
858
569
  }
859
570
 
860
571
  try {
572
+ // Immediately clear welcome message and show user request
573
+ this.clearWelcomeMessage();
574
+
861
575
  const taskData = {
862
576
  task_description: taskDescription,
863
577
  llm_profile_name: llmProfile
864
578
  };
865
579
 
866
580
  // Add uploaded files path if any
867
- if (this.state.uploadedFiles.length > 0) {
868
- console.log('[UIManager] Raw uploaded files state:', this.state.uploadedFiles);
869
-
870
- // Extract the first file path (backend expects single string)
871
- const firstFile = this.state.uploadedFiles[0];
872
- let filePath = null;
873
-
874
- if (typeof firstFile === 'string') {
875
- filePath = firstFile;
876
- } else if (firstFile && typeof firstFile === 'object') {
877
- // Extract path and normalize
878
- filePath = firstFile.file_path || firstFile.path || firstFile.stored_filename || firstFile.file_path;
879
- if (filePath) {
880
- filePath = filePath.replace(/\\/g, '/');
881
- console.log('[UIManager] Normalized file path:', filePath);
882
- }
883
- }
884
-
885
- if (filePath) {
886
- // Backend expects 'upload_files_path' as a single string
887
- taskData.upload_files_path = filePath;
888
- console.log('[UIManager] Set upload_files_path to:', filePath);
889
-
890
- // Show info if multiple files uploaded but only first will be processed
891
- if (this.state.uploadedFiles.length > 1) {
892
- console.warn('[UIManager] Multiple files uploaded, but backend only supports single file. Using first file:', filePath);
893
- this.showNotification(`Multiple files uploaded. Only the first file "${firstFile.name || filePath}" will be processed.`, 'warning');
894
- }
895
- } else {
896
- console.error('[UIManager] Could not extract file path from uploaded file:', firstFile);
897
- }
581
+ const filePath = this.fileManager.getUploadedFilesForTask();
582
+ if (filePath) {
583
+ taskData.upload_files_path = filePath;
584
+ console.log('[UIManager] Set upload_files_path to:', filePath);
898
585
  }
899
586
 
900
587
  console.log('[UIManager] Complete task data being submitted:', JSON.stringify(taskData, null, 2));
901
588
  await this.sessionManager.submitTask(taskData);
902
589
 
903
590
  // Clear uploaded files after successful task submission
904
- this.clearUploadedFiles();
591
+ this.fileManager.clearUploadedFiles();
905
592
  } catch (error) {
906
593
  this.showNotification(`Failed to submit task: ${error.message}`, 'error');
907
594
  }
@@ -931,238 +618,37 @@ class VibeSurfUIManager {
931
618
  }
932
619
  }
933
620
 
934
- handleAttachFiles() {
935
- this.elements.fileInput?.click();
936
- }
937
-
938
- async handleFileSelection(event) {
939
- const files = Array.from(event.target.files);
940
-
941
- if (files.length === 0) return;
942
-
943
- try {
944
- this.showLoading(`Uploading ${files.length} file(s)...`);
945
-
946
- const response = await this.sessionManager.uploadFiles(files);
947
-
948
- console.log('[UIManager] File upload response:', response);
949
-
950
- // If SessionManager doesn't trigger the event, handle it directly
951
- if (response && response.files) {
952
- console.log('[UIManager] Manually handling uploaded files');
953
- this.handleFilesUploaded({
954
- sessionId: this.sessionManager.getCurrentSessionId(),
955
- files: response.files
956
- });
957
- }
958
-
959
- this.hideLoading();
960
- this.showNotification(`${files.length} file(s) uploaded successfully`, 'success');
961
-
962
- // Clear file input
963
- event.target.value = '';
964
- } catch (error) {
965
- this.hideLoading();
966
- this.showNotification(`File upload failed: ${error.message}`, 'error');
621
+ handleTaskInputKeydown(event) {
622
+ if (event.key === 'Enter' && !event.shiftKey) {
623
+ event.preventDefault();
624
+ this.handleSendTask();
967
625
  }
968
626
  }
969
627
 
970
- handleFilesUploaded(data) {
971
- console.log('[UIManager] Files uploaded event received:', data);
972
-
973
- // Ensure data.files is always an array - handle both single file and array cases
974
- let filesArray = [];
975
- if (data.files) {
976
- if (Array.isArray(data.files)) {
977
- filesArray = data.files;
978
- } else {
979
- // If single file object, wrap in array
980
- filesArray = [data.files];
981
- console.log('[UIManager] Single file detected, wrapping in array');
982
- }
983
- }
984
-
985
- console.log('[UIManager] Processing files array:', filesArray);
986
-
987
- if (filesArray.length > 0) {
988
- // Append new files to existing uploaded files (for multiple uploads)
989
- const newFiles = filesArray.map(file => ({
990
- id: file.file_id,
991
- name: file.original_filename,
992
- path: file.file_path, // Updated to use file_path field
993
- size: file.file_size,
994
- type: file.mime_type,
995
- stored_filename: file.stored_filename,
996
- file_path: file.file_path // Add file_path for backward compatibility
997
- }));
998
-
999
- console.log('[UIManager] Mapped new files:', newFiles);
1000
-
1001
- // Add to existing files instead of replacing
1002
- this.state.uploadedFiles = [...this.state.uploadedFiles, ...newFiles];
1003
-
1004
- console.log('[UIManager] Updated uploaded files state:', this.state.uploadedFiles);
1005
-
1006
- // Update the visual file list
1007
- this.updateFilesList();
1008
- } else {
1009
- console.warn('[UIManager] No files to process in uploaded data');
628
+ handleLlmProfileChange(event) {
629
+ // Re-validate send button state when LLM profile changes
630
+ if (this.elements.taskInput) {
631
+ this.handleTaskInputChange({ target: this.elements.taskInput });
1010
632
  }
1011
633
  }
1012
634
 
1013
- updateFilesList() {
1014
- const container = this.getOrCreateFilesListContainer();
1015
-
1016
- // Debug logging to identify the issue
1017
- console.log('[UIManager] updateFilesList called');
1018
- console.log('[UIManager] uploadedFiles type:', typeof this.state.uploadedFiles);
1019
- console.log('[UIManager] uploadedFiles isArray:', Array.isArray(this.state.uploadedFiles));
1020
- console.log('[UIManager] uploadedFiles value:', this.state.uploadedFiles);
1021
-
1022
- // Ensure uploadedFiles is always an array
1023
- if (!Array.isArray(this.state.uploadedFiles)) {
1024
- console.error('[UIManager] uploadedFiles is not an array, resetting to empty array');
1025
- this.state.uploadedFiles = [];
1026
- }
635
+ handleTaskInputChange(event) {
636
+ const hasText = event.target.value.trim().length > 0;
637
+ const textarea = event.target;
638
+ const llmProfile = this.elements.llmProfileSelect?.value;
639
+ const hasLlmProfile = llmProfile && llmProfile.trim() !== '';
1027
640
 
1028
- if (this.state.uploadedFiles.length === 0) {
1029
- container.style.display = 'none';
1030
- return;
641
+ // Update send button state - require both text and LLM profile and no running task
642
+ if (this.elements.sendBtn) {
643
+ this.elements.sendBtn.disabled = !(hasText && hasLlmProfile && !this.state.isTaskRunning);
1031
644
  }
1032
645
 
1033
- container.style.display = 'block';
1034
-
1035
- // Build HTML safely with proper validation
1036
- let filesHTML = '';
1037
- try {
1038
- filesHTML = this.state.uploadedFiles.map((file, index) => {
1039
- console.log(`[UIManager] Processing file ${index}:`, file);
1040
-
1041
- // Validate file object structure
1042
- if (!file || typeof file !== 'object') {
1043
- console.error(`[UIManager] Invalid file object at index ${index}:`, file);
1044
- return '';
1045
- }
1046
-
1047
- // Extract properties safely with fallbacks
1048
- const fileId = file.id || file.file_id || `file_${index}`;
1049
- const fileName = file.name || file.original_filename || 'Unknown file';
1050
- const filePath = file.path || file.file_path || file.stored_filename || 'Unknown path';
1051
-
1052
- console.log(`[UIManager] File display data: id=${fileId}, name=${fileName}, path=${filePath}`);
1053
-
1054
- return `
1055
- <div class="file-item" data-file-id="${fileId}">
1056
- <span class="file-name" title="${filePath}">${fileName}</span>
1057
- <button class="file-remove-btn" title="Remove file" data-file-id="${fileId}">×</button>
1058
- </div>
1059
- `;
1060
- }).join('');
1061
- } catch (error) {
1062
- console.error('[UIManager] Error generating files HTML:', error);
1063
- filesHTML = '<div class="error-message">Error displaying files</div>';
1064
- }
1065
-
1066
- container.innerHTML = `
1067
- <div class="files-items">
1068
- ${filesHTML}
1069
- </div>
1070
- `;
1071
-
1072
- // Add event listeners for remove buttons
1073
- container.querySelectorAll('.file-remove-btn').forEach(btn => {
1074
- btn.addEventListener('click', (e) => {
1075
- e.preventDefault();
1076
- const fileId = btn.dataset.fileId;
1077
- this.removeUploadedFile(fileId);
1078
- });
1079
- });
1080
- }
1081
-
1082
- getOrCreateFilesListContainer() {
1083
- let container = document.getElementById('uploaded-files-list');
1084
-
1085
- if (!container) {
1086
- container = document.createElement('div');
1087
- container.id = 'uploaded-files-list';
1088
- container.className = 'uploaded-files-container';
1089
-
1090
- // Insert after the textarea-container to avoid affecting button layout
1091
- if (this.elements.taskInput) {
1092
- const textareaContainer = this.elements.taskInput.closest('.textarea-container');
1093
- if (textareaContainer && textareaContainer.parentElement) {
1094
- // Insert after the textarea-container but before the input-footer
1095
- const inputFooter = textareaContainer.parentElement.querySelector('.input-footer');
1096
- if (inputFooter) {
1097
- textareaContainer.parentElement.insertBefore(container, inputFooter);
1098
- } else {
1099
- textareaContainer.parentElement.insertBefore(container, textareaContainer.nextSibling);
1100
- }
1101
- }
1102
- }
1103
- }
1104
-
1105
- return container;
1106
- }
1107
-
1108
- removeUploadedFile(fileId) {
1109
- console.log('[UIManager] Removing uploaded file:', fileId);
1110
-
1111
- // Remove from state
1112
- this.state.uploadedFiles = this.state.uploadedFiles.filter(file => file.id !== fileId);
1113
-
1114
- // Update visual list
1115
- this.updateFilesList();
1116
-
1117
- this.showNotification('File removed from upload list', 'info');
1118
- }
1119
-
1120
- formatFileSize(bytes) {
1121
- if (!bytes) return '0 B';
1122
-
1123
- const k = 1024;
1124
- const sizes = ['B', 'KB', 'MB', 'GB'];
1125
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1126
-
1127
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
1128
- }
1129
-
1130
- clearUploadedFiles() {
1131
- this.state.uploadedFiles = [];
1132
- this.updateFilesList();
1133
- }
1134
-
1135
- handleTaskInputKeydown(event) {
1136
- if (event.key === 'Enter' && !event.shiftKey) {
1137
- event.preventDefault();
1138
- this.handleSendTask();
1139
- }
1140
- }
1141
-
1142
- handleLlmProfileChange(event) {
1143
- // Re-validate send button state when LLM profile changes
1144
- if (this.elements.taskInput) {
1145
- this.handleTaskInputChange({ target: this.elements.taskInput });
1146
- }
1147
- }
1148
-
1149
- handleTaskInputChange(event) {
1150
- const hasText = event.target.value.trim().length > 0;
1151
- const textarea = event.target;
1152
- const llmProfile = this.elements.llmProfileSelect?.value;
1153
- const hasLlmProfile = llmProfile && llmProfile.trim() !== '';
1154
-
1155
- // Update send button state - require both text and LLM profile and no running task
1156
- if (this.elements.sendBtn) {
1157
- this.elements.sendBtn.disabled = !(hasText && hasLlmProfile && !this.state.isTaskRunning);
1158
- }
1159
-
1160
- // Auto-resize textarea based on content
1161
- this.autoResizeTextarea(textarea);
1162
- }
1163
-
1164
- autoResizeTextarea(textarea) {
1165
- if (!textarea) return;
646
+ // Auto-resize textarea based on content
647
+ this.autoResizeTextarea(textarea);
648
+ }
649
+
650
+ autoResizeTextarea(textarea) {
651
+ if (!textarea) return;
1166
652
 
1167
653
  // Reset height to auto to get the natural scrollHeight
1168
654
  textarea.style.height = 'auto';
@@ -1176,2539 +662,553 @@ class VibeSurfUIManager {
1176
662
  textarea.style.height = newHeight + 'px';
1177
663
  }
1178
664
 
1179
- async handleBackendUrlChange(event) {
1180
- const newUrl = event.target.value.trim();
1181
-
1182
- if (!newUrl) {
1183
- this.showNotification('Backend URL cannot be empty', 'warning');
1184
- return;
1185
- }
1186
-
1187
- try {
1188
- // Validate URL format
1189
- new URL(newUrl);
1190
-
1191
- // Update API client
1192
- this.apiClient.setBaseURL(newUrl);
1193
-
1194
- // Save to settings via main app
1195
- if (window.vibeSurfApp) {
1196
- await window.vibeSurfApp.updateSettings({ backendUrl: newUrl });
1197
- }
1198
-
1199
- this.showNotification('Backend URL updated successfully', 'success');
1200
- console.log('[UIManager] Backend URL updated to:', newUrl);
1201
-
1202
- } catch (error) {
1203
- this.showNotification(`Invalid backend URL: ${error.message}`, 'error');
1204
- console.error('[UIManager] Backend URL update failed:', error);
1205
- }
1206
- }
1207
-
1208
- // Settings Tab Management
1209
- handleTabSwitch(event) {
1210
- const clickedTab = event.currentTarget;
1211
- const targetTabId = clickedTab.dataset.tab;
1212
-
1213
- // Update tab buttons
1214
- this.elements.settingsTabs?.forEach(tab => {
1215
- tab.classList.remove('active');
1216
- });
1217
- clickedTab.classList.add('active');
1218
-
1219
- // Update tab content
1220
- this.elements.settingsTabContents?.forEach(content => {
1221
- content.classList.remove('active');
1222
- });
1223
- const targetContent = document.getElementById(`${targetTabId}-tab`);
1224
- if (targetContent) {
1225
- targetContent.classList.add('active');
1226
- }
1227
- }
1228
-
1229
- // Profile Management
1230
- async handleAddProfile(type) {
1231
- try {
1232
- this.showProfileForm(type);
1233
- } catch (error) {
1234
- console.error(`[UIManager] Failed to show ${type} profile form:`, error);
1235
- this.showNotification(`Failed to show ${type} profile form`, 'error');
1236
- }
1237
- }
1238
-
1239
- async showProfileForm(type, profile = null) {
1240
- const isEdit = profile !== null;
1241
- const title = isEdit ? `Edit ${type.toUpperCase()} Profile` : `Add ${type.toUpperCase()} Profile`;
1242
-
1243
- if (this.elements.profileFormTitle) {
1244
- this.elements.profileFormTitle.textContent = title;
1245
- }
1246
-
1247
- // Generate form content based on type
1248
- let formHTML = '';
1249
- if (type === 'llm') {
1250
- formHTML = await this.generateLLMProfileForm(profile);
1251
- } else if (type === 'mcp') {
1252
- formHTML = this.generateMCPProfileForm(profile);
1253
- }
1254
-
1255
- if (this.elements.profileForm) {
1256
- this.elements.profileForm.innerHTML = formHTML;
1257
- this.elements.profileForm.dataset.type = type;
1258
- this.elements.profileForm.dataset.mode = isEdit ? 'edit' : 'create';
1259
- if (isEdit && profile) {
1260
- this.elements.profileForm.dataset.profileId = profile.profile_name || profile.mcp_id;
1261
- }
1262
- }
1263
-
1264
- // Setup form event listeners
1265
- this.setupProfileFormEvents();
1266
-
1267
- // Show modal
1268
- if (this.elements.profileFormModal) {
1269
- this.elements.profileFormModal.classList.remove('hidden');
1270
- }
1271
- }
1272
-
1273
- async generateLLMProfileForm(profile = null) {
1274
- // Fetch available providers
1275
- let providers = [];
1276
- try {
1277
- const response = await this.apiClient.getLLMProviders();
1278
- providers = response.providers || response || [];
1279
- } catch (error) {
1280
- console.error('[UIManager] Failed to fetch LLM providers:', error);
1281
- }
1282
-
1283
- const providersOptions = providers.map(p =>
1284
- `<option value="${p.name}" ${profile?.provider === p.name ? 'selected' : ''}>${p.display_name}</option>`
1285
- ).join('');
1286
-
1287
- const selectedProvider = profile?.provider || (providers.length > 0 ? providers[0].name : '');
1288
- const selectedProviderData = providers.find(p => p.name === selectedProvider);
1289
- const models = selectedProviderData?.models || [];
1290
-
1291
- const modelsOptions = models.map(model =>
1292
- `<option value="${model}" ${profile?.model === model ? 'selected' : ''}>${model}</option>`
1293
- ).join('');
1294
-
1295
- return `
1296
- <div class="form-group">
1297
- <label class="form-label required">Profile Name</label>
1298
- <input type="text" name="profile_name" class="form-input" value="${profile?.profile_name || ''}"
1299
- placeholder="Enter a unique name for this profile" required ${profile ? 'readonly' : ''}>
1300
- <div class="form-help">A unique identifier for this LLM configuration</div>
1301
- </div>
1302
-
1303
- <div class="form-group">
1304
- <label class="form-label required">Provider</label>
1305
- <select name="provider" class="form-select" required>
1306
- <option value="">Select a provider</option>
1307
- ${providersOptions}
1308
- </select>
1309
- <div class="form-help">Choose your LLM provider (OpenAI, Anthropic, etc.)</div>
1310
- </div>
1311
-
1312
- <div class="form-group">
1313
- <label class="form-label required">Model</label>
1314
- <input type="text" name="model" class="form-input model-input" value="${profile?.model || ''}"
1315
- list="model-options" placeholder="Select a model or type custom model name" required
1316
- autocomplete="off">
1317
- <datalist id="model-options">
1318
- ${models.map(model => `<option value="${model}">${model}</option>`).join('')}
1319
- </datalist>
1320
- <div class="form-help">Choose from the list or enter a custom model name</div>
1321
- </div>
1322
-
1323
- <div class="form-group api-key-field">
1324
- <label class="form-label required">API Key</label>
1325
- <input type="password" name="api_key" class="form-input api-key-input"
1326
- placeholder="${profile ? 'Leave empty to keep existing key' : 'Enter your API key'}"
1327
- ${profile ? '' : 'required'}>
1328
- <button type="button" class="api-key-toggle" title="Toggle visibility">
1329
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1330
- <path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/>
1331
- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
1332
- </svg>
1333
- </button>
1334
- <div class="form-help">Your provider's API key for authentication</div>
1335
- </div>
1336
-
1337
- <div class="form-group">
1338
- <label class="form-label">Base URL</label>
1339
- <input type="url" name="base_url" class="form-input" value="${profile?.base_url || ''}"
1340
- placeholder="https://api.openai.com/v1">
1341
- <div class="form-help">Custom API endpoint (leave empty for provider default)</div>
1342
- </div>
1343
-
1344
- <div class="form-group">
1345
- <label class="form-label">Temperature</label>
1346
- <input type="number" name="temperature" class="form-input" value="${profile?.temperature || ''}"
1347
- min="0" max="2" step="0.1" placeholder="0.7">
1348
- <div class="form-help">Controls randomness (0.0-2.0, lower = more focused)</div>
1349
- </div>
1350
-
1351
- <div class="form-group">
1352
- <label class="form-label">Max Tokens</label>
1353
- <input type="number" name="max_tokens" class="form-input" value="${profile?.max_tokens || ''}"
1354
- min="1" max="128000" placeholder="4096">
1355
- <div class="form-help">Maximum tokens in the response</div>
1356
- </div>
1357
-
1358
- <div class="form-group">
1359
- <label class="form-label">Description</label>
1360
- <textarea name="description" class="form-textarea" placeholder="Optional description for this profile">${profile?.description || ''}</textarea>
1361
- <div class="form-help">Optional description to help identify this profile</div>
1362
- </div>
1363
-
1364
- <div class="form-group">
1365
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1366
- <input type="checkbox" name="is_default" ${profile?.is_default ? 'checked' : ''}>
1367
- <span class="form-label" style="margin: 0;">Set as default profile</span>
1368
- </label>
1369
- <div class="form-help">This profile will be selected by default for new tasks</div>
1370
- </div>
1371
- `;
1372
- }
1373
-
1374
- generateMCPProfileForm(profile = null) {
1375
- // Convert existing profile to JSON for editing
1376
- let defaultJson = '{\n "command": "npx",\n "args": [\n "-y",\n "@modelcontextprotocol/server-filesystem",\n "/path/to/directory"\n ]\n}';
1377
-
1378
- if (profile?.mcp_server_params) {
1379
- try {
1380
- defaultJson = JSON.stringify(profile.mcp_server_params, null, 2);
1381
- } catch (error) {
1382
- console.warn('[UIManager] Failed to stringify existing mcp_server_params:', error);
1383
- }
1384
- }
1385
-
1386
- return `
1387
- <div class="form-group">
1388
- <label class="form-label required">Display Name</label>
1389
- <input type="text" name="display_name" class="form-input" value="${profile?.display_name || ''}"
1390
- placeholder="Enter a friendly name for this MCP profile" required ${profile ? 'readonly' : ''}>
1391
- <div class="form-help">A user-friendly name for this MCP configuration</div>
1392
- </div>
1393
-
1394
- <div class="form-group">
1395
- <label class="form-label required">Server Name</label>
1396
- <input type="text" name="mcp_server_name" class="form-input" value="${profile?.mcp_server_name || ''}"
1397
- placeholder="e.g., filesystem, markitdown, brave-search" required>
1398
- <div class="form-help">The MCP server identifier</div>
1399
- </div>
1400
-
1401
- <div class="form-group">
1402
- <label class="form-label required">MCP Server Parameters (JSON)</label>
1403
- <textarea name="mcp_server_params_json" class="form-textarea json-input" rows="8"
1404
- placeholder="Enter JSON configuration for MCP server parameters" required>${defaultJson}</textarea>
1405
- <div class="json-validation-feedback"></div>
1406
- <div class="form-help">
1407
- JSON configuration including command and arguments. Example:
1408
- <br><code>{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]}</code>
1409
- </div>
1410
- </div>
1411
-
1412
- <div class="form-group">
1413
- <label class="form-label">Description</label>
1414
- <textarea name="description" class="form-textarea" placeholder="Optional description for this MCP profile">${profile?.description || ''}</textarea>
1415
- <div class="form-help">Optional description to help identify this profile</div>
1416
- </div>
1417
-
1418
- <div class="form-group">
1419
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1420
- <input type="checkbox" name="is_active" ${profile?.is_active !== false ? 'checked' : ''}>
1421
- <span class="form-label" style="margin: 0;">Active</span>
1422
- </label>
1423
- <div class="form-help">Whether this MCP profile is active and available for use</div>
1424
- </div>
1425
- `;
1426
- }
1427
-
1428
- setupProfileFormEvents() {
1429
- console.log('[UIManager] Setting up profile form events');
1430
-
1431
- // Add form submission handler first (directly, no cloning)
1432
- if (this.elements.profileForm) {
1433
- this.elements.profileForm.addEventListener('submit', this.handleProfileFormSubmit.bind(this));
1434
- console.log('[UIManager] Form submit listener added');
1435
- }
1436
-
1437
- // Provider change handler for LLM profiles
1438
- const providerSelect = this.elements.profileForm?.querySelector('select[name="provider"]');
1439
- if (providerSelect) {
1440
- providerSelect.addEventListener('change', this.handleProviderChange.bind(this));
1441
- console.log('[UIManager] Provider select change listener added');
1442
- }
1443
-
1444
- // API key toggle handler
1445
- const apiKeyToggle = this.elements.profileForm?.querySelector('.api-key-toggle');
1446
- const apiKeyInput = this.elements.profileForm?.querySelector('.api-key-input');
1447
- if (apiKeyToggle && apiKeyInput) {
1448
- apiKeyToggle.addEventListener('click', () => {
1449
- const isPassword = apiKeyInput.type === 'password';
1450
- apiKeyInput.type = isPassword ? 'text' : 'password';
1451
-
1452
- // Update icon
1453
- const svg = apiKeyToggle.querySelector('svg');
1454
- if (svg) {
1455
- svg.innerHTML = isPassword ?
1456
- '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20C7 20 2.73 16.39 1 12A18.45 18.45 0 0 1 5.06 5.06L17.94 17.94ZM9.9 4.24A9.12 9.12 0 0 1 12 4C17 4 21.27 7.61 23 12A18.5 18.5 0 0 1 19.42 16.42" stroke="currentColor" stroke-width="2" fill="none"/><path d="M1 1L23 23" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" fill="none"/>' :
1457
- '<path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>';
1458
- }
1459
- });
1460
- console.log('[UIManager] API key toggle listener added');
1461
- }
1462
-
1463
- // JSON validation handler for MCP profiles
1464
- const jsonInput = this.elements.profileForm?.querySelector('textarea[name="mcp_server_params_json"]');
1465
- if (jsonInput) {
1466
- jsonInput.addEventListener('input', this.handleJsonInputValidation.bind(this));
1467
- jsonInput.addEventListener('blur', this.handleJsonInputValidation.bind(this));
1468
- console.log('[UIManager] JSON validation listener added');
1469
-
1470
- // Trigger initial validation
1471
- this.handleJsonInputValidation({ target: jsonInput });
1472
- }
1473
- }
1474
-
1475
- handleJsonInputValidation(event) {
1476
- const textarea = event.target;
1477
- const feedbackElement = textarea.parentElement.querySelector('.json-validation-feedback');
1478
-
1479
- if (!feedbackElement) return;
1480
-
1481
- const jsonText = textarea.value.trim();
1482
-
1483
- if (!jsonText) {
1484
- feedbackElement.innerHTML = '';
1485
- textarea.classList.remove('json-valid', 'json-invalid');
1486
- return;
1487
- }
1488
-
1489
- try {
1490
- const parsed = JSON.parse(jsonText);
1491
-
1492
- // Validate that it's an object (not array, string, etc.)
1493
- if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
1494
- throw new Error('MCP server parameters must be a JSON object');
1495
- }
1496
-
1497
- // Validate required fields
1498
- if (!parsed.command || typeof parsed.command !== 'string') {
1499
- throw new Error('Missing or invalid "command" field (must be a string)');
1500
- }
1501
-
1502
- // Validate args if present
1503
- if (parsed.args && !Array.isArray(parsed.args)) {
1504
- throw new Error('"args" field must be an array if provided');
1505
- }
1506
-
1507
- // Success
1508
- feedbackElement.innerHTML = '<span class="json-success">✓ Valid JSON configuration</span>';
1509
- textarea.classList.remove('json-invalid');
1510
- textarea.classList.add('json-valid');
1511
-
1512
- // Store valid state for form submission
1513
- textarea.dataset.isValid = 'true';
1514
-
1515
- } catch (error) {
1516
- const errorMessage = error.message;
1517
- feedbackElement.innerHTML = `<span class="json-error">✗ Invalid JSON: ${errorMessage}</span>`;
1518
- textarea.classList.remove('json-valid');
1519
- textarea.classList.add('json-invalid');
1520
-
1521
- // Store invalid state for form submission
1522
- textarea.dataset.isValid = 'false';
1523
- textarea.dataset.errorMessage = errorMessage;
1524
-
1525
- // Show error modal for critical validation errors - trigger on both blur and input events
1526
- if ((event.type === 'blur' || event.type === 'input') && jsonText.length > 0) {
1527
- console.log('[UIManager] JSON validation failed, showing error modal:', errorMessage);
1528
- setTimeout(() => {
1529
- this.showJsonValidationErrorModal(errorMessage);
1530
- }, event.type === 'blur' ? 100 : 500); // Longer delay for input events
1531
- }
1532
- }
1533
- }
1534
-
1535
- showJsonValidationErrorModal(errorMessage) {
1536
- console.log('[UIManager] Creating JSON validation error modal');
1537
- const modal = this.createWarningModal({
1538
- title: 'JSON Validation Error',
1539
- message: 'The MCP server parameters contain invalid JSON format.',
1540
- details: `Error: ${errorMessage}\n\nPlease correct the JSON format before submitting the form.`,
1541
- buttons: [
1542
- {
1543
- text: 'OK',
1544
- style: 'primary',
1545
- action: () => {
1546
- console.log('[UIManager] JSON validation error modal OK clicked');
1547
- this.closeWarningModal();
1548
- }
1549
- }
1550
- ]
1551
- });
1552
-
1553
- console.log('[UIManager] Appending JSON validation error modal to body');
1554
- document.body.appendChild(modal);
1555
- }
1556
-
1557
- // Add separate method to handle submit button clicks
1558
- handleProfileFormSubmitClick(event) {
1559
- console.log('[UIManager] Profile form submit button clicked');
1560
- event.preventDefault();
1561
-
1562
- // Find the form and trigger submit
1563
- const form = this.elements.profileForm;
1564
- if (form) {
1565
- console.log('[UIManager] Triggering form submit via button click');
1566
- const submitEvent = new Event('submit', { cancelable: true, bubbles: true });
1567
- form.dispatchEvent(submitEvent);
665
+ // UI Display Methods
666
+ updateSessionDisplay(sessionId) {
667
+ console.log('[UIManager] Updating session display with ID:', sessionId);
668
+ if (this.elements.sessionId) {
669
+ this.elements.sessionId.textContent = sessionId || '-';
670
+ console.log('[UIManager] Session ID display updated to:', sessionId);
1568
671
  } else {
1569
- console.error('[UIManager] Profile form not found when submit button clicked');
672
+ console.error('[UIManager] Session ID element not found');
1570
673
  }
1571
674
  }
1572
675
 
1573
- async handleProviderChange(event) {
1574
- const selectedProvider = event.target.value;
1575
- const modelInput = this.elements.profileForm?.querySelector('input[name="model"]');
1576
- const modelDatalist = this.elements.profileForm?.querySelector('#model-options');
676
+ updateControlPanel(status) {
677
+ const panel = this.elements.controlPanel;
678
+ const cancelBtn = this.elements.cancelBtn;
679
+ const resumeBtn = this.elements.resumeBtn;
680
+ const terminateBtn = this.elements.terminateBtn;
1577
681
 
1578
- console.log('[UIManager] Provider changed to:', selectedProvider);
682
+ console.log(`[UIManager] updateControlPanel called with status: ${status}`);
1579
683
 
1580
- if (!selectedProvider || !modelInput || !modelDatalist) {
1581
- console.warn('[UIManager] Missing elements for provider change');
684
+ if (!panel) {
685
+ console.error('[UIManager] Control panel element not found');
1582
686
  return;
1583
687
  }
1584
688
 
1585
- // Always clear the model input when provider changes
1586
- modelInput.value = '';
1587
- modelInput.placeholder = `Loading ${selectedProvider} models...`;
1588
- modelDatalist.innerHTML = '<option value="">Loading...</option>';
689
+ // Clear any existing auto-hide timeout
690
+ if (this.controlPanelTimeout) {
691
+ clearTimeout(this.controlPanelTimeout);
692
+ this.controlPanelTimeout = null;
693
+ }
1589
694
 
1590
- try {
1591
- console.log('[UIManager] Fetching models for provider:', selectedProvider);
1592
- const response = await this.apiClient.getLLMProviderModels(selectedProvider);
1593
- const models = response.models || response || [];
1594
-
1595
- console.log('[UIManager] Received models:', models);
1596
-
1597
- // Update datalist options
1598
- modelDatalist.innerHTML = models.map(model =>
1599
- `<option value="${model}">${model}</option>`
1600
- ).join('');
1601
-
1602
- // Update placeholder to reflect the new provider
1603
- modelInput.placeholder = models.length > 0
1604
- ? `Select a ${selectedProvider} model or type custom model name`
1605
- : `Enter ${selectedProvider} model name`;
695
+ switch (status) {
696
+ case 'ready':
697
+ console.log('[UIManager] Setting control panel to ready (hidden)');
698
+ panel.classList.add('hidden');
699
+ panel.classList.remove('error-state');
700
+ break;
1606
701
 
1607
- console.log('[UIManager] Model list updated for provider:', selectedProvider);
702
+ case 'running':
703
+ console.log('[UIManager] Setting control panel to running (showing cancel button)');
704
+ panel.classList.remove('hidden');
705
+ panel.classList.remove('error-state');
706
+ cancelBtn?.classList.remove('hidden');
707
+ resumeBtn?.classList.add('hidden');
708
+ terminateBtn?.classList.add('hidden');
1608
709
 
1609
- } catch (error) {
1610
- console.error('[UIManager] Failed to fetch models for provider:', error);
1611
- modelDatalist.innerHTML = '<option value="">Failed to load models</option>';
1612
- modelInput.placeholder = `Enter ${selectedProvider} model name manually`;
1613
-
1614
- // Show user-friendly error notification
1615
- this.showNotification(`Failed to load models for ${selectedProvider}. You can enter the model name manually.`, 'warning');
1616
- }
1617
- }
1618
-
1619
- closeProfileForm() {
1620
- if (this.elements.profileFormModal) {
1621
- this.elements.profileFormModal.classList.add('hidden');
1622
- }
1623
- }
1624
-
1625
- async handleProfileFormSubmit(event) {
1626
- event.preventDefault();
1627
- console.log('[UIManager] Profile form submit triggered');
1628
-
1629
- const form = event.target;
1630
-
1631
- // Prevent multiple submissions
1632
- if (form.dataset.submitting === 'true') {
1633
- console.log('[UIManager] Form already submitting, ignoring duplicate submission');
1634
- return;
1635
- }
1636
-
1637
- const formData = new FormData(form);
1638
- const type = form.dataset.type;
1639
- const mode = form.dataset.mode;
1640
- const profileId = form.dataset.profileId;
1641
-
1642
- console.log('[UIManager] Form submission details:', { type, mode, profileId });
1643
-
1644
- // Set submitting state and disable form
1645
- form.dataset.submitting = 'true';
1646
- this.setProfileFormSubmitting(true);
1647
-
1648
- // Convert FormData to object
1649
- const data = {};
1650
-
1651
- // Handle checkbox fields explicitly first
1652
- const checkboxFields = ['is_default', 'is_active'];
1653
- checkboxFields.forEach(fieldName => {
1654
- const checkbox = form.querySelector(`input[name="${fieldName}"]`);
1655
- if (checkbox) {
1656
- data[fieldName] = checkbox.checked;
1657
- console.log(`[UIManager] Checkbox field: ${fieldName} = ${checkbox.checked}`);
1658
- }
1659
- });
1660
-
1661
- for (const [key, value] of formData.entries()) {
1662
- console.log(`[UIManager] Form field: ${key} = ${value}`);
1663
- if (value.trim() !== '') {
1664
- if (key === 'args' && type === 'mcp') {
1665
- // Split args by newlines for MCP profiles
1666
- data[key] = value.split('\n').map(arg => arg.trim()).filter(arg => arg);
1667
- } else if (key === 'is_default' || key === 'is_active') {
1668
- // Skip - already handled above with explicit checkbox checking
1669
- continue;
1670
- } else if (key === 'temperature') {
1671
- const num = parseFloat(value);
1672
- if (!isNaN(num) && num >= 0) {
1673
- data[key] = num;
1674
- }
1675
- // 如果不设置或无效值,就不传给后端
1676
- } else if (key === 'max_tokens') {
1677
- const num = parseInt(value);
1678
- if (!isNaN(num) && num > 0) {
1679
- data[key] = num;
1680
- }
1681
- // Max Tokens如果不设置的话,不用传到后端
1682
- } else {
1683
- data[key] = value;
1684
- }
1685
- }
1686
- }
1687
-
1688
- console.log('[UIManager] Processed form data:', data);
1689
-
1690
- // Handle MCP server params structure - parse JSON input
1691
- if (type === 'mcp') {
1692
- const jsonInput = data.mcp_server_params_json;
1693
-
1694
- // Check if JSON was pre-validated
1695
- const jsonTextarea = form.querySelector('textarea[name="mcp_server_params_json"]');
1696
- if (jsonTextarea && jsonTextarea.dataset.isValid === 'false') {
1697
- console.error('[UIManager] JSON validation failed during form submission');
1698
- this.showJsonValidationErrorModal(jsonTextarea.dataset.errorMessage || 'Invalid JSON format');
1699
- return; // Don't submit the form if JSON is invalid
1700
- }
1701
-
1702
- if (jsonInput) {
1703
- try {
1704
- const parsedParams = JSON.parse(jsonInput);
1705
-
1706
- // Validate the parsed JSON structure
1707
- if (typeof parsedParams !== 'object' || Array.isArray(parsedParams) || parsedParams === null) {
1708
- throw new Error('MCP server parameters must be a JSON object');
1709
- }
1710
-
1711
- if (!parsedParams.command || typeof parsedParams.command !== 'string') {
1712
- throw new Error('Missing or invalid "command" field (must be a string)');
1713
- }
1714
-
1715
- if (parsedParams.args && !Array.isArray(parsedParams.args)) {
1716
- throw new Error('"args" field must be an array if provided');
1717
- }
1718
-
1719
- // Set the parsed parameters
1720
- data.mcp_server_params = parsedParams;
1721
- console.log('[UIManager] MCP server params parsed from JSON:', data.mcp_server_params);
1722
-
1723
- } catch (error) {
1724
- console.error('[UIManager] Failed to parse MCP server params JSON:', error);
1725
- this.showJsonValidationErrorModal(error.message);
1726
- return; // Don't submit the form if JSON is invalid
1727
- }
1728
- } else {
1729
- console.error('[UIManager] Missing mcp_server_params_json in form data');
1730
- this.showNotification('MCP server parameters JSON is required', 'error');
1731
- return;
1732
- }
1733
-
1734
- // Remove the JSON field as it's not needed in the API request
1735
- delete data.mcp_server_params_json;
1736
- console.log('[UIManager] MCP data structure updated:', data);
1737
- }
1738
-
1739
- try {
1740
- console.log(`[UIManager] Starting ${mode} operation for ${type} profile`);
1741
- let response;
1742
- const endpoint = type === 'llm' ? '/config/llm-profiles' : '/config/mcp-profiles';
1743
-
1744
- if (mode === 'create') {
1745
- console.log('[UIManager] Creating new profile...');
1746
- if (type === 'llm') {
1747
- response = await this.apiClient.createLLMProfile(data);
1748
- } else {
1749
- response = await this.apiClient.createMCPProfile(data);
1750
- }
1751
- } else {
1752
- console.log('[UIManager] Updating existing profile...');
1753
- if (type === 'llm') {
1754
- response = await this.apiClient.updateLLMProfile(profileId, data);
1755
- } else {
1756
- response = await this.apiClient.updateMCPProfile(profileId, data);
1757
- }
1758
- }
1759
-
1760
- console.log('[UIManager] API response:', response);
1761
-
1762
- this.closeProfileForm();
1763
- this.showNotification(`${type.toUpperCase()} profile ${mode === 'create' ? 'created' : 'updated'} successfully`, 'success');
1764
-
1765
- console.log('[UIManager] Refreshing settings data...');
1766
- // Refresh the settings data
1767
- await this.loadSettingsData();
1768
- console.log('[UIManager] Settings data refreshed');
1769
-
1770
- // Force re-render of MCP profiles to ensure status is updated
1771
- if (type === 'mcp') {
1772
- console.log('[UIManager] Force updating MCP profiles display');
1773
- this.renderMCPProfiles(this.state.mcpProfiles);
1774
- }
1775
-
1776
- } catch (error) {
1777
- console.error(`[UIManager] Failed to ${mode} ${type} profile:`, error);
1778
-
1779
- // Handle specific error types for better user experience
1780
- let errorMessage = error.message || 'Unknown error occurred';
1781
-
1782
- if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
1783
- // For duplicate profile name errors, highlight the name field
1784
- this.highlightProfileNameError(errorMessage);
1785
- errorMessage = errorMessage; // Use the specific error message from backend
1786
- } else if (errorMessage.includes('UNIQUE constraint')) {
1787
- errorMessage = `Profile name '${data.profile_name || data.display_name}' already exists. Please choose a different name.`;
1788
- this.highlightProfileNameError(errorMessage);
1789
- }
1790
-
1791
- this.showNotification(`Failed to ${mode} ${type} profile: ${errorMessage}`, 'error');
1792
- } finally {
1793
- // Reset form state
1794
- form.dataset.submitting = 'false';
1795
- this.setProfileFormSubmitting(false);
1796
- }
1797
- }
1798
-
1799
- setProfileFormSubmitting(isSubmitting) {
1800
- const form = this.elements.profileForm;
1801
- const submitButton = this.elements.profileFormSubmit;
1802
- const cancelButton = this.elements.profileFormCancel;
1803
-
1804
- if (!form) return;
1805
-
1806
- // Disable/enable form inputs
1807
- const inputs = form.querySelectorAll('input, select, textarea');
1808
- inputs.forEach(input => {
1809
- input.disabled = isSubmitting;
1810
- });
1811
-
1812
- // Update submit button
1813
- if (submitButton) {
1814
- submitButton.disabled = isSubmitting;
1815
- submitButton.textContent = isSubmitting ? 'Saving...' : 'Save Profile';
1816
- }
1817
-
1818
- // Update cancel button
1819
- if (cancelButton) {
1820
- cancelButton.disabled = isSubmitting;
1821
- }
1822
-
1823
- console.log(`[UIManager] Profile form submitting state: ${isSubmitting}`);
1824
- }
1825
-
1826
- highlightProfileNameError(errorMessage) {
1827
- const nameInput = this.elements.profileForm?.querySelector('input[name="profile_name"], input[name="display_name"]');
1828
-
1829
- if (nameInput) {
1830
- // Add error styling
1831
- nameInput.classList.add('form-error');
1832
- nameInput.focus();
1833
-
1834
- // Create or update error message
1835
- let errorElement = nameInput.parentElement.querySelector('.profile-name-error');
1836
- if (!errorElement) {
1837
- errorElement = document.createElement('div');
1838
- errorElement.className = 'form-error-message profile-name-error';
1839
- nameInput.parentElement.appendChild(errorElement);
1840
- }
1841
-
1842
- errorElement.textContent = errorMessage;
1843
-
1844
- // Remove error styling after user starts typing
1845
- const removeError = () => {
1846
- nameInput.classList.remove('form-error');
1847
- if (errorElement) {
1848
- errorElement.remove();
1849
- }
1850
- nameInput.removeEventListener('input', removeError);
1851
- };
1852
-
1853
- nameInput.addEventListener('input', removeError);
1854
-
1855
- console.log('[UIManager] Highlighted profile name error:', errorMessage);
1856
- }
1857
- }
1858
-
1859
- // Real-time profile name validation
1860
- async validateProfileNameAvailability(profileName, profileType) {
1861
- if (!profileName || profileName.trim().length < 2) {
1862
- return { isValid: true, message: '' }; // Don't validate very short names
1863
- }
1864
-
1865
- try {
1866
- // Check if profile already exists
1867
- const profiles = profileType === 'llm' ? this.state.llmProfiles : this.state.mcpProfiles;
1868
- const nameField = profileType === 'llm' ? 'profile_name' : 'display_name';
1869
-
1870
- const existingProfile = profiles.find(profile =>
1871
- profile[nameField].toLowerCase() === profileName.toLowerCase()
1872
- );
1873
-
1874
- if (existingProfile) {
1875
- return {
1876
- isValid: false,
1877
- message: `${profileType.toUpperCase()} profile "${profileName}" already exists. Please choose a different name.`
1878
- };
1879
- }
1880
-
1881
- return { isValid: true, message: '' };
1882
- } catch (error) {
1883
- console.error('[UIManager] Error validating profile name:', error);
1884
- return { isValid: true, message: '' }; // Don't block on validation errors
1885
- }
1886
- }
1887
-
1888
-
1889
- // Settings Data Management
1890
- async loadSettingsData() {
1891
- try {
1892
- // Load LLM profiles
1893
- await this.loadLLMProfiles();
1894
-
1895
- // Load MCP profiles
1896
- await this.loadMCPProfiles();
1897
-
1898
- // Load environment variables
1899
- await this.loadEnvironmentVariables();
1900
-
1901
- // Update LLM profile select dropdown
1902
- this.updateLLMProfileSelect();
1903
-
1904
- } catch (error) {
1905
- console.error('[UIManager] Failed to load settings data:', error);
1906
- this.showNotification('Failed to load settings data', 'error');
1907
- }
1908
- }
1909
-
1910
- async loadLLMProfiles() {
1911
- try {
1912
- const response = await this.apiClient.getLLMProfiles(false); // Load all profiles, not just active
1913
- console.log('[UIManager] LLM profiles loaded:', response);
1914
-
1915
- // Handle different response structures
1916
- let profiles = [];
1917
- if (Array.isArray(response)) {
1918
- profiles = response;
1919
- } else if (response.profiles && Array.isArray(response.profiles)) {
1920
- profiles = response.profiles;
1921
- } else if (response.data && Array.isArray(response.data)) {
1922
- profiles = response.data;
1923
- }
1924
-
1925
- this.state.llmProfiles = profiles;
1926
- this.renderLLMProfiles(profiles);
1927
- this.updateLLMProfileSelect();
1928
- } catch (error) {
1929
- console.error('[UIManager] Failed to load LLM profiles:', error);
1930
- this.state.llmProfiles = [];
1931
- this.renderLLMProfiles([]);
1932
- this.updateLLMProfileSelect();
1933
- }
1934
- }
1935
-
1936
- async loadMCPProfiles() {
1937
- try {
1938
- const response = await this.apiClient.getMCPProfiles(false); // Load all profiles, not just active
1939
- console.log('[UIManager] MCP profiles loaded:', response);
1940
-
1941
- // Handle different response structures
1942
- let profiles = [];
1943
- if (Array.isArray(response)) {
1944
- profiles = response;
1945
- } else if (response.profiles && Array.isArray(response.profiles)) {
1946
- profiles = response.profiles;
1947
- } else if (response.data && Array.isArray(response.data)) {
1948
- profiles = response.data;
1949
- }
1950
-
1951
- this.state.mcpProfiles = profiles;
1952
- this.renderMCPProfiles(profiles);
1953
- } catch (error) {
1954
- console.error('[UIManager] Failed to load MCP profiles:', error);
1955
- this.state.mcpProfiles = [];
1956
- this.renderMCPProfiles([]);
1957
- }
1958
- }
1959
-
1960
- async loadEnvironmentVariables() {
1961
- try {
1962
- const response = await this.apiClient.getEnvironmentVariables();
1963
- console.log('[UIManager] Environment variables loaded:', response);
1964
- const envVars = response.environments || response || {};
1965
- this.renderEnvironmentVariables(envVars);
1966
- } catch (error) {
1967
- console.error('[UIManager] Failed to load environment variables:', error);
1968
- this.renderEnvironmentVariables({});
1969
- }
1970
- }
1971
-
1972
- renderLLMProfiles(profiles) {
1973
- const container = document.getElementById('llm-profiles-list');
1974
- if (!container) return;
1975
-
1976
- if (profiles.length === 0) {
1977
- container.innerHTML = `
1978
- <div class="empty-state">
1979
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1980
- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1981
- <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1982
- <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1983
- </svg>
1984
- <h3>No LLM Profiles</h3>
1985
- <p>Create your first LLM profile to get started</p>
1986
- </div>
1987
- `;
1988
- return;
1989
- }
1990
-
1991
- const profilesHTML = profiles.map(profile => `
1992
- <div class="profile-card ${profile.is_default ? 'default' : ''}" data-profile-id="${profile.profile_name}">
1993
- ${profile.is_default ? '<div class="profile-badge">Default</div>' : ''}
1994
- <div class="profile-header">
1995
- <div class="profile-title">
1996
- <h3>${this.escapeHtml(profile.profile_name)}</h3>
1997
- <span class="profile-provider">${this.escapeHtml(profile.provider)}</span>
1998
- </div>
1999
- <div class="profile-actions">
2000
- <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
2001
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2002
- <path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2003
- <path d="M18.5 2.5C18.8978 2.10217 19.4374 1.87868 20 1.87868C20.5626 1.87868 21.1022 2.10217 21.5 2.5C21.8978 2.89783 22.1213 3.43739 22.1213 4C22.1213 4.56261 21.8978 5.10217 21.5 5.5L12 15L8 16L9 12L18.5 2.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2004
- </svg>
2005
- </button>
2006
- <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.profile_name}">
2007
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2008
- <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2009
- <path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2010
- </svg>
2011
- </button>
2012
- </div>
2013
- </div>
2014
- <div class="profile-content">
2015
- <div class="profile-info">
2016
- <span class="profile-model">${this.escapeHtml(profile.model)}</span>
2017
- ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
2018
- </div>
2019
- <div class="profile-details">
2020
- ${profile.base_url ? `<div class="profile-detail"><strong>Base URL:</strong> ${this.escapeHtml(profile.base_url)}</div>` : ''}
2021
- ${profile.temperature !== undefined ? `<div class="profile-detail"><strong>Temperature:</strong> ${profile.temperature}</div>` : ''}
2022
- ${profile.max_tokens ? `<div class="profile-detail"><strong>Max Tokens:</strong> ${profile.max_tokens}</div>` : ''}
2023
- </div>
2024
- </div>
2025
- </div>
2026
- `).join('');
2027
-
2028
- container.innerHTML = profilesHTML;
2029
-
2030
- // Add event listeners for profile actions
2031
- container.querySelectorAll('.edit').forEach(btn => {
2032
- btn.addEventListener('click', (e) => {
2033
- e.stopPropagation();
2034
- const profile = JSON.parse(btn.dataset.profile);
2035
- this.showProfileForm('llm', profile);
2036
- });
2037
- });
2038
-
2039
- container.querySelectorAll('.delete').forEach(btn => {
2040
- btn.addEventListener('click', async (e) => {
2041
- e.stopPropagation();
2042
- await this.handleDeleteProfile('llm', btn.dataset.profileId);
2043
- });
2044
- });
2045
- }
2046
-
2047
- renderMCPProfiles(profiles) {
2048
- const container = document.getElementById('mcp-profiles-list');
2049
- if (!container) return;
2050
-
2051
- if (profiles.length === 0) {
2052
- container.innerHTML = `
2053
- <div class="empty-state">
2054
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2055
- <path d="M8 2V8M16 2V8M3 10H21M5 4H19C20.1046 4 21 4.89543 21 6V20C21 21.1046 20.1046 22 19 22H5C3.89543 22 3 21.1046 3 20V6C3 4.89543 3.89543 4 5 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2056
- </svg>
2057
- <h3>No MCP Profiles</h3>
2058
- <p>Create your first MCP profile to enable server integrations</p>
2059
- </div>
2060
- `;
2061
- return;
2062
- }
2063
-
2064
- const profilesHTML = profiles.map(profile => `
2065
- <div class="profile-card ${profile.is_active ? 'active' : 'inactive'}" data-profile-id="${profile.mcp_id}">
2066
- <div class="profile-status ${profile.is_active ? 'active' : 'inactive'}">
2067
- ${profile.is_active ? 'Active' : 'Inactive'}
2068
- </div>
2069
- <div class="profile-header">
2070
- <div class="profile-title">
2071
- <h3>${this.escapeHtml(profile.display_name)}</h3>
2072
- <span class="profile-provider">${this.escapeHtml(profile.mcp_server_name)}</span>
2073
- </div>
2074
- <div class="profile-actions">
2075
- <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
2076
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2077
- <path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2078
- <path d="M18.5 2.5C18.8978 2.10217 19.4374 1.87868 20 1.87868C20.5626 1.87868 21.1022 2.10217 21.5 2.5C21.8978 2.89783 22.1213 3.43739 22.1213 4C22.1213 4.56261 21.8978 5.10217 21.5 5.5L12 15L8 16L9 12L18.5 2.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2079
- </svg>
2080
- </button>
2081
- <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.mcp_id}">
2082
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2083
- <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2084
- <path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2085
- </svg>
2086
- </button>
2087
- </div>
2088
- </div>
2089
- <div class="profile-content">
2090
- ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
2091
- <div class="profile-details">
2092
- <div class="profile-detail"><strong>Command:</strong> ${this.escapeHtml(profile.mcp_server_params?.command || 'N/A')}</div>
2093
- ${profile.mcp_server_params?.args?.length ? `<div class="profile-detail"><strong>Args:</strong> ${profile.mcp_server_params.args.join(', ')}</div>` : ''}
2094
- </div>
2095
- </div>
2096
- </div>
2097
- `).join('');
2098
-
2099
- container.innerHTML = profilesHTML;
2100
-
2101
- // Add event listeners for profile actions
2102
- container.querySelectorAll('.edit').forEach(btn => {
2103
- btn.addEventListener('click', (e) => {
2104
- e.stopPropagation();
2105
- const profile = JSON.parse(btn.dataset.profile);
2106
- this.showProfileForm('mcp', profile);
2107
- });
2108
- });
2109
-
2110
- container.querySelectorAll('.delete').forEach(btn => {
2111
- btn.addEventListener('click', async (e) => {
2112
- e.stopPropagation();
2113
- await this.handleDeleteProfile('mcp', btn.dataset.profileId);
2114
- });
2115
- });
2116
- }
2117
-
2118
- renderEnvironmentVariables(envVars) {
2119
- const container = this.elements.envVariablesList;
2120
- if (!container) return;
2121
-
2122
- // Clear existing content
2123
- container.innerHTML = '';
2124
-
2125
- // Check if there are any environment variables to display
2126
- if (Object.keys(envVars).length === 0) {
2127
- container.innerHTML = `
2128
- <div class="empty-state">
2129
- <div class="empty-state-icon">🔧</div>
2130
- <div class="empty-state-title">No Environment Variables</div>
2131
- <div class="empty-state-description">Environment variables are configured on the backend. Only updates to existing variables are allowed.</div>
2132
- </div>
2133
- `;
2134
- return;
2135
- }
2136
-
2137
- // Backend URL related keys that should be readonly
2138
- const backendUrlKeys = [
2139
- 'BACKEND_URL',
2140
- 'VIBESURF_BACKEND_URL',
2141
- 'API_URL',
2142
- 'BASE_URL',
2143
- 'API_BASE_URL',
2144
- 'BACKEND_API_URL'
2145
- ];
2146
-
2147
- // Add existing environment variables (read-only keys, editable/readonly values based on type)
2148
- Object.entries(envVars).forEach(([key, value]) => {
2149
- const envVarItem = document.createElement('div');
2150
- envVarItem.className = 'env-var-item';
2151
-
2152
- // Check if this is a backend URL variable
2153
- const isBackendUrl = backendUrlKeys.includes(key.toUpperCase());
2154
- const valueReadonly = isBackendUrl ? 'readonly' : '';
2155
- const valueClass = isBackendUrl ? 'form-input readonly-input' : 'form-input';
2156
- const valueTitle = isBackendUrl ? 'Backend URL is not editable from settings' : '';
2157
-
2158
- envVarItem.innerHTML = `
2159
- <div class="env-var-key">
2160
- <input type="text" class="form-input" placeholder="Variable name" value="${this.escapeHtml(key)}" readonly>
2161
- </div>
2162
- <div class="env-var-value">
2163
- <input type="text" class="${valueClass}" placeholder="Variable value" value="${this.escapeHtml(value)}" ${valueReadonly} title="${valueTitle}">
2164
- </div>
2165
- `;
2166
-
2167
- container.appendChild(envVarItem);
2168
- });
2169
- }
2170
-
2171
- async handleDeleteProfile(type, profileId) {
2172
- // Check if this is a default LLM profile
2173
- if (type === 'llm') {
2174
- const profile = this.state.llmProfiles.find(p => p.profile_name === profileId);
2175
- if (profile && profile.is_default) {
2176
- // Handle default profile deletion differently
2177
- return await this.handleDeleteDefaultProfile(profileId);
2178
- }
2179
- }
2180
-
2181
- // Create a modern confirmation modal instead of basic confirm()
2182
- return new Promise((resolve) => {
2183
- const modal = this.createWarningModal({
2184
- title: `Delete ${type.toUpperCase()} Profile`,
2185
- message: `Are you sure you want to delete the "${profileId}" profile? This action cannot be undone.`,
2186
- details: `This will permanently remove the ${type.toUpperCase()} profile and all its configurations.`,
2187
- buttons: [
2188
- {
2189
- text: 'Delete Profile',
2190
- style: 'danger',
2191
- action: async () => {
2192
- this.closeWarningModal();
2193
- await this.performDeleteProfile(type, profileId);
2194
- resolve(true);
2195
- }
2196
- },
2197
- {
2198
- text: 'Cancel',
2199
- style: 'secondary',
2200
- action: () => {
2201
- this.closeWarningModal();
2202
- resolve(false);
2203
- }
2204
- }
2205
- ]
2206
- });
2207
-
2208
- document.body.appendChild(modal);
2209
- });
2210
- }
2211
-
2212
- async handleDeleteDefaultProfile(profileId) {
2213
- // Get other available profiles
2214
- const otherProfiles = this.state.llmProfiles.filter(p => p.profile_name !== profileId);
2215
-
2216
- if (otherProfiles.length === 0) {
2217
- // No other profiles available - cannot delete
2218
- const modal = this.createWarningModal({
2219
- title: 'Cannot Delete Default Profile',
2220
- message: 'This is the only LLM profile configured. You cannot delete it without having at least one other profile.',
2221
- details: 'Please create another LLM profile first, then you can delete this one.',
2222
- buttons: [
2223
- {
2224
- text: 'Create New Profile',
2225
- style: 'primary',
2226
- action: () => {
2227
- this.closeWarningModal();
2228
- this.handleAddProfile('llm');
2229
- }
2230
- },
2231
- {
2232
- text: 'Cancel',
2233
- style: 'secondary',
2234
- action: () => {
2235
- this.closeWarningModal();
2236
- }
2237
- }
2238
- ]
2239
- });
2240
-
2241
- document.body.appendChild(modal);
2242
- return false;
2243
- }
2244
-
2245
- // Show modal to select new default profile
2246
- return new Promise((resolve) => {
2247
- const profileOptions = otherProfiles.map(profile =>
2248
- `<label style="display: flex; align-items: center; gap: 8px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin: 4px 0; cursor: pointer;">
2249
- <input type="radio" name="newDefault" value="${profile.profile_name}" required>
2250
- <div>
2251
- <div style="font-weight: bold;">${profile.profile_name}</div>
2252
- <div style="font-size: 12px; color: #666;">${profile.provider} - ${profile.model}</div>
2253
- </div>
2254
- </label>`
2255
- ).join('');
2256
-
2257
- const modal = this.createWarningModal({
2258
- title: 'Delete Default Profile',
2259
- message: `"${profileId}" is currently the default profile. Please select a new default profile before deleting it.`,
2260
- details: `<div style="margin: 16px 0;">
2261
- <div style="font-weight: bold; margin-bottom: 8px;">Select new default profile:</div>
2262
- <form id="newDefaultForm" style="max-height: 200px; overflow-y: auto;">
2263
- ${profileOptions}
2264
- </form>
2265
- </div>`,
2266
- buttons: [
2267
- {
2268
- text: 'Set Default & Delete',
2269
- style: 'danger',
2270
- action: async () => {
2271
- const form = document.getElementById('newDefaultForm');
2272
- const selectedProfile = form.querySelector('input[name="newDefault"]:checked');
2273
-
2274
- if (!selectedProfile) {
2275
- this.showNotification('Please select a new default profile', 'warning');
2276
- return;
2277
- }
2278
-
2279
- try {
2280
- this.closeWarningModal();
2281
- await this.setNewDefaultAndDelete(selectedProfile.value, profileId);
2282
- resolve(true);
2283
- } catch (error) {
2284
- this.showNotification(`Failed to update default profile: ${error.message}`, 'error');
2285
- resolve(false);
2286
- }
2287
- }
2288
- },
2289
- {
2290
- text: 'Cancel',
2291
- style: 'secondary',
2292
- action: () => {
2293
- this.closeWarningModal();
2294
- resolve(false);
2295
- }
2296
- }
2297
- ]
2298
- });
2299
-
2300
- document.body.appendChild(modal);
2301
- });
2302
- }
2303
-
2304
- async setNewDefaultAndDelete(newDefaultProfileId, profileToDelete) {
2305
- try {
2306
- this.showLoading('Updating default profile...');
2307
-
2308
- // First, set the new default profile
2309
- await this.apiClient.updateLLMProfile(newDefaultProfileId, { is_default: true });
2310
-
2311
- this.showLoading('Deleting profile...');
2312
-
2313
- // Then delete the old default profile
2314
- await this.apiClient.deleteLLMProfile(profileToDelete);
2315
-
2316
- this.showNotification(`Profile "${profileToDelete}" deleted and "${newDefaultProfileId}" set as default`, 'success');
2317
-
2318
- // Refresh the settings data
2319
- await this.loadSettingsData();
2320
-
2321
- this.hideLoading();
2322
- } catch (error) {
2323
- this.hideLoading();
2324
- console.error('[UIManager] Failed to set new default and delete profile:', error);
2325
- this.showNotification(`Failed to update profiles: ${error.message}`, 'error');
2326
- throw error;
2327
- }
2328
- }
2329
-
2330
- async performDeleteProfile(type, profileId) {
2331
- try {
2332
- this.showLoading(`Deleting ${type} profile...`);
2333
-
2334
- if (type === 'llm') {
2335
- await this.apiClient.deleteLLMProfile(profileId);
2336
- } else {
2337
- await this.apiClient.deleteMCPProfile(profileId);
2338
- }
2339
-
2340
- this.showNotification(`${type.toUpperCase()} profile deleted successfully`, 'success');
2341
-
2342
- // Refresh the settings data
2343
- await this.loadSettingsData();
2344
-
2345
- this.hideLoading();
2346
- } catch (error) {
2347
- this.hideLoading();
2348
- console.error(`[UIManager] Failed to delete ${type} profile:`, error);
2349
- this.showNotification(`Failed to delete ${type} profile: ${error.message}`, 'error');
2350
- }
2351
- }
2352
-
2353
- async handleSaveEnvironmentVariables() {
2354
- if (!this.elements.envVariablesList) return;
2355
-
2356
- const envVarItems = this.elements.envVariablesList.querySelectorAll('.env-var-item');
2357
- const envVars = {};
2358
-
2359
- // Backend URL related keys that should be skipped during save
2360
- const backendUrlKeys = [
2361
- 'BACKEND_URL',
2362
- 'VIBESURF_BACKEND_URL',
2363
- 'API_URL',
2364
- 'BASE_URL',
2365
- 'API_BASE_URL',
2366
- 'BACKEND_API_URL'
2367
- ];
2368
-
2369
- envVarItems.forEach(item => {
2370
- const keyInput = item.querySelector('.env-var-key input');
2371
- const valueInput = item.querySelector('.env-var-value input');
2372
-
2373
- if (keyInput && valueInput && keyInput.value.trim()) {
2374
- const key = keyInput.value.trim();
2375
- const value = valueInput.value.trim();
2376
-
2377
- // Skip backend URL variables (they are readonly)
2378
- if (!backendUrlKeys.includes(key.toUpperCase())) {
2379
- envVars[key] = value;
2380
- }
2381
- }
2382
- });
2383
-
2384
- try {
2385
- await this.apiClient.updateEnvironmentVariables(envVars);
2386
- this.showNotification('Environment variables updated successfully (backend URL variables are read-only)', 'success');
2387
- } catch (error) {
2388
- console.error('[UIManager] Failed to update environment variables:', error);
2389
- this.showNotification(`Failed to update environment variables: ${error.message}`, 'error');
2390
- }
2391
- }
2392
-
2393
- escapeHtml(text) {
2394
- if (typeof text !== 'string') return '';
2395
- const div = document.createElement('div');
2396
- div.textContent = text;
2397
- return div.innerHTML;
2398
- }
2399
-
2400
- // UI Display Methods
2401
- updateSessionDisplay(sessionId) {
2402
- console.log('[UIManager] Updating session display with ID:', sessionId);
2403
- if (this.elements.sessionId) {
2404
- this.elements.sessionId.textContent = sessionId || '-';
2405
- console.log('[UIManager] Session ID display updated to:', sessionId);
2406
- } else {
2407
- console.error('[UIManager] Session ID element not found');
2408
- }
2409
- }
2410
-
2411
- updateControlPanel(status) {
2412
- const panel = this.elements.controlPanel;
2413
- const cancelBtn = this.elements.cancelBtn;
2414
- const resumeBtn = this.elements.resumeBtn;
2415
- const terminateBtn = this.elements.terminateBtn;
2416
-
2417
- console.log(`[UIManager] updateControlPanel called with status: ${status}`);
2418
-
2419
- if (!panel) {
2420
- console.error('[UIManager] Control panel element not found');
2421
- return;
2422
- }
2423
-
2424
- // Clear any existing auto-hide timeout
2425
- if (this.controlPanelTimeout) {
2426
- clearTimeout(this.controlPanelTimeout);
2427
- this.controlPanelTimeout = null;
2428
- }
2429
-
2430
- switch (status) {
2431
- case 'ready':
2432
- console.log('[UIManager] Setting control panel to ready (hidden)');
2433
- panel.classList.add('hidden');
2434
- panel.classList.remove('error-state');
2435
- break;
2436
-
2437
- case 'running':
2438
- console.log('[UIManager] Setting control panel to running (showing cancel button)');
2439
- panel.classList.remove('hidden');
2440
- panel.classList.remove('error-state');
2441
- cancelBtn?.classList.remove('hidden');
2442
- resumeBtn?.classList.add('hidden');
2443
- terminateBtn?.classList.add('hidden');
2444
-
2445
- // For fast-completing tasks, ensure minimum visibility duration
2446
- this.ensureMinimumControlPanelVisibility();
2447
- break;
2448
-
2449
- case 'paused':
2450
- console.log('[UIManager] Setting control panel to paused (showing resume/terminate buttons)');
2451
- panel.classList.remove('hidden');
2452
- panel.classList.remove('error-state');
2453
- cancelBtn?.classList.add('hidden');
2454
- resumeBtn?.classList.remove('hidden');
2455
- terminateBtn?.classList.remove('hidden');
2456
- break;
2457
-
2458
- case 'error':
2459
- console.log('[UIManager] Setting control panel to error (keeping cancel/terminate buttons visible)');
2460
- panel.classList.remove('hidden');
2461
- panel.classList.add('error-state');
2462
- cancelBtn?.classList.remove('hidden');
2463
- resumeBtn?.classList.add('hidden');
2464
- terminateBtn?.classList.remove('hidden');
2465
- break;
2466
-
2467
- default:
2468
- console.log(`[UIManager] Unknown control panel status: ${status}, hiding panel`);
2469
- panel.classList.add('hidden');
2470
- panel.classList.remove('error-state');
2471
- }
2472
- }
2473
-
2474
- ensureMinimumControlPanelVisibility() {
2475
- // Set a flag to prevent immediate hiding of control panel for fast tasks
2476
- this.controlPanelMinVisibilityActive = true;
2477
-
2478
- // Clear the flag after minimum visibility period (2 seconds)
2479
- setTimeout(() => {
2480
- this.controlPanelMinVisibilityActive = false;
2481
- console.log('[UIManager] Minimum control panel visibility period ended');
2482
- }, 2000);
2483
- }
2484
-
2485
- clearActivityLog() {
2486
- if (this.elements.activityLog) {
2487
- this.elements.activityLog.innerHTML = '';
2488
- }
2489
- }
2490
-
2491
- showWelcomeMessage() {
2492
- const welcomeHTML = `
2493
- <div class="welcome-message">
2494
- <div class="welcome-text">
2495
- <h4>Welcome to VibeSurf</h4>
2496
- <p>Let's vibe surfing the world with AI automation</p>
2497
- </div>
2498
- <div class="quick-tasks">
2499
- <div class="task-suggestion" data-task="research">
2500
- <div class="task-icon">🔍</div>
2501
- <div class="task-content">
2502
- <div class="task-title">Research Founders</div>
2503
- <div class="task-description">Search information about browser-use and browser-use-webui, write a brief report</div>
2504
- </div>
2505
- </div>
2506
- <div class="task-suggestion" data-task="news">
2507
- <div class="task-icon">📰</div>
2508
- <div class="task-content">
2509
- <div class="task-title">HackerNews Summary</div>
2510
- <div class="task-description">Get top 10 news from HackerNews and provide a summary</div>
2511
- </div>
2512
- </div>
2513
- <div class="task-suggestion" data-task="analysis">
2514
- <div class="task-icon">📈</div>
2515
- <div class="task-content">
2516
- <div class="task-title">Stock Market Analysis</div>
2517
- <div class="task-description">Analyze recent week stock market trends for major tech companies</div>
2518
- </div>
2519
- </div>
2520
- </div>
2521
- </div>
2522
- `;
2523
-
2524
- if (this.elements.activityLog) {
2525
- this.elements.activityLog.innerHTML = welcomeHTML;
2526
- this.bindTaskSuggestionEvents();
2527
- }
2528
- }
2529
-
2530
- bindTaskSuggestionEvents() {
2531
- const taskSuggestions = document.querySelectorAll('.task-suggestion');
2532
-
2533
- taskSuggestions.forEach(suggestion => {
2534
- suggestion.addEventListener('click', () => {
2535
- const taskDescription = suggestion.querySelector('.task-description').textContent;
2536
-
2537
- // Populate task input with suggestion (only description, no title prefix)
2538
- if (this.elements.taskInput) {
2539
- this.elements.taskInput.value = taskDescription;
2540
- this.elements.taskInput.focus();
2541
-
2542
- // Trigger input change event for validation and auto-resize
2543
- this.handleTaskInputChange({ target: this.elements.taskInput });
2544
-
2545
- // Auto-send the task
2546
- setTimeout(() => {
2547
- this.handleSendTask();
2548
- }, 100); // Small delay to ensure input processing is complete
2549
- }
2550
- });
2551
- });
2552
- }
2553
-
2554
- displayActivityLogs(logs) {
2555
- this.clearActivityLog();
2556
-
2557
- if (logs.length === 0) {
2558
- this.showWelcomeMessage();
2559
- return;
2560
- }
2561
-
2562
- logs.forEach(log => this.addActivityLog(log));
2563
- this.scrollActivityToBottom();
2564
- }
2565
-
2566
- addActivityLog(activityData) {
2567
-
2568
- // Filter out "done" status messages from UI display only
2569
- const agentStatus = activityData.agent_status || activityData.status || '';
2570
- const agentMsg = activityData.agent_msg || activityData.message || '';
2571
-
2572
-
2573
- // Filter done messages that are just status updates without meaningful content
2574
- if (agentStatus.toLowerCase() === 'done') {
2575
- return;
2576
- }
2577
-
2578
- const activityItem = this.createActivityItem(activityData);
2579
-
2580
- if (this.elements.activityLog) {
2581
- // Remove welcome message if present
2582
- const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
2583
- if (welcomeMsg) {
2584
- welcomeMsg.remove();
2585
- }
2586
-
2587
- this.elements.activityLog.appendChild(activityItem);
2588
- activityItem.classList.add('fade-in');
2589
- }
2590
- }
2591
-
2592
- addActivityItem(data) {
2593
- const activityItem = this.createActivityItem(data);
2594
-
2595
- if (this.elements.activityLog) {
2596
- // Remove welcome message if present
2597
- const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
2598
- if (welcomeMsg) {
2599
- welcomeMsg.remove();
2600
- }
2601
-
2602
- this.elements.activityLog.appendChild(activityItem);
2603
- activityItem.classList.add('fade-in');
2604
- this.scrollActivityToBottom();
2605
- }
2606
- }
2607
-
2608
- createActivityItem(data) {
2609
- const item = document.createElement('div');
2610
-
2611
- // Extract activity data with correct keys
2612
- const agentName = data.agent_name || 'system';
2613
- const agentStatus = data.agent_status || data.status || 'info';
2614
- const agentMsg = data.agent_msg || data.message || data.action_description || 'No description';
2615
- const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString();
2616
-
2617
- // Determine if this is a user message (should be on the right)
2618
- const isUser = agentName.toLowerCase() === 'user';
2619
-
2620
- // Set CSS classes based on agent type and status
2621
- item.className = `activity-item ${isUser ? 'user-message' : 'agent-message'} ${agentStatus}`;
2622
-
2623
- // Create the message structure similar to chat interface
2624
- item.innerHTML = `
2625
- <div class="message-container ${isUser ? 'user-container' : 'agent-container'}">
2626
- <div class="message-header">
2627
- <span class="agent-name">${agentName}</span>
2628
- <span class="message-time">${timestamp}</span>
2629
- </div>
2630
- <div class="message-bubble ${isUser ? 'user-bubble' : 'agent-bubble'}">
2631
- <div class="message-status">
2632
- <span class="status-indicator ${agentStatus}">${this.getStatusIcon(agentStatus)}</span>
2633
- <span class="status-text">${agentStatus}</span>
2634
- </div>
2635
- <div class="message-content">
2636
- ${this.formatActivityContent(agentMsg)}
2637
- </div>
2638
- </div>
2639
- </div>
2640
- `;
2641
-
2642
- return item;
2643
- }
2644
-
2645
- getStatusIcon(status) {
2646
- switch (status.toLowerCase()) {
2647
- case 'working':
2648
- return '⚙️';
2649
- case 'thinking':
2650
- return '🤔';
2651
- case 'result':
2652
- case 'done':
2653
- return '✅';
2654
- case 'error':
2655
- return '❌';
2656
- case 'paused':
2657
- return '⏸️';
2658
- case 'running':
2659
- return '🔄';
2660
- case 'request':
2661
- return '💡';
2662
- default:
2663
- return '💡';
2664
- }
2665
- }
2666
-
2667
- formatActivityContent(content) {
2668
-
2669
- if (!content) return 'No content';
2670
-
2671
- // Handle object content
2672
- if (typeof content === 'object') {
2673
- return `<pre class="json-content">${JSON.stringify(content, null, 2)}</pre>`;
2674
- }
2675
-
2676
- // Convert content to string
2677
- let formattedContent = String(content);
2678
-
2679
- // Use markdown-it library for proper markdown rendering if available
2680
- if (typeof markdownit !== 'undefined') {
2681
- try {
2682
- // Create markdown-it instance with enhanced options
2683
- const md = markdownit({
2684
- html: true, // Enable HTML tags in source
2685
- breaks: true, // Convert '\n' in paragraphs into <br>
2686
- linkify: true, // Autoconvert URL-like text to links
2687
- typographer: true // Enable some language-neutral replacement + quotes beautification
2688
- });
2689
-
2690
- // Override link renderer to handle file:// protocol
2691
- const defaultLinkRenderer = md.renderer.rules.link_open || function(tokens, idx, options, env, renderer) {
2692
- return renderer.renderToken(tokens, idx, options);
2693
- };
2694
-
2695
- md.renderer.rules.link_open = function (tokens, idx, options, env, renderer) {
2696
- const token = tokens[idx];
2697
- const href = token.attrGet('href');
2698
-
2699
- if (href && href.startsWith('file://')) {
2700
- // Convert file:// links to our custom file-link format
2701
- token.attrSet('href', '#');
2702
- token.attrSet('class', 'file-link');
2703
- token.attrSet('data-file-path', href);
2704
- token.attrSet('title', `Click to open file: ${href}`);
2705
- }
2706
-
2707
- return defaultLinkRenderer(tokens, idx, options, env, renderer);
2708
- };
2709
-
2710
- // Add task list support manually (markdown-it doesn't have built-in task lists)
2711
- formattedContent = this.preprocessTaskLists(formattedContent);
2712
-
2713
- // Pre-process file:// markdown links since markdown-it doesn't recognize them
2714
- const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+)\)/g;
2715
- formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2716
- // Convert to HTML format that markdown-it will preserve
2717
- return `<a href="${fileUrl}" class="file-link-markdown">${linkText}</a>`;
2718
- });
2719
-
2720
- // Parse markdown
2721
- const htmlContent = md.render(formattedContent);
2722
-
2723
- // Post-process to handle local file path links (Windows paths and file:// URLs)
2724
- let processedContent = htmlContent;
2725
-
2726
- // Convert our pre-processed file:// links to proper file-link format
2727
- const preProcessedFileLinkRegex = /<a href="(file:\/\/[^"]+)"[^>]*class="file-link-markdown"[^>]*>([^<]*)<\/a>/g;
2728
- processedContent = processedContent.replace(preProcessedFileLinkRegex, (match, fileUrl, linkText) => {
2729
- try {
2730
- // Decode and fix the file URL
2731
- let decodedUrl = decodeURIComponent(fileUrl);
2732
- let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2733
- cleanPath = cleanPath.replace(/\\/g, '/');
2734
-
2735
- // Ensure path starts with / for Unix paths or has drive letter for Windows
2736
- if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
2737
- cleanPath = '/' + cleanPath;
2738
- }
2739
-
2740
- // Recreate proper file URL - always use triple slash for proper format
2741
- let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2742
- `file:///${cleanPath}` :
2743
- `file://${cleanPath}`;
2744
-
2745
-
2746
- return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2747
- } catch (error) {
2748
- console.error('[UIManager] Error processing pre-processed file:// link:', error);
2749
- return match;
2750
- }
2751
- });
2752
-
2753
- // Detect and convert local Windows file path links
2754
- const windowsPathLinkRegex = /<a href="([A-Za-z]:\\[^"]+\.html?)"([^>]*)>([^<]*)<\/a>/g;
2755
- processedContent = processedContent.replace(windowsPathLinkRegex, (match, filePath, attributes, linkText) => {
2756
-
2757
- try {
2758
- // Convert Windows path to file:// URL
2759
- let normalizedPath = filePath.replace(/\\/g, '/');
2760
- let fileUrl = `file:///${normalizedPath}`;
2761
-
2762
-
2763
- return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file: ${filePath}"${attributes}>${linkText}</a>`;
2764
- } catch (error) {
2765
- console.error('[UIManager] Error converting Windows path:', error);
2766
- return match; // Return original if conversion fails
2767
- }
2768
- });
2769
-
2770
- // Detect and convert file:// protocol links
2771
- const fileProtocolLinkRegex = /<a href="(file:\/\/[^"]+\.html?)"([^>]*)>([^<]*)<\/a>/g;
2772
- processedContent = processedContent.replace(fileProtocolLinkRegex, (match, fileUrl, attributes, linkText) => {
2773
-
2774
- try {
2775
- // Decode and fix the file URL
2776
- let decodedUrl = decodeURIComponent(fileUrl);
2777
- let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2778
- cleanPath = cleanPath.replace(/\\/g, '/');
2779
-
2780
- // Recreate proper file URL
2781
- let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2782
- `file:///${cleanPath}` :
2783
- `file://${cleanPath}`;
2784
-
2785
-
2786
- return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file"${attributes}>${linkText}</a>`;
2787
- } catch (error) {
2788
- console.error('[UIManager] Error processing file:// link:', error);
2789
- return match; // Return original if conversion fails
2790
- }
2791
- });
2792
-
2793
- // Add custom classes and post-process
2794
- return processedContent
2795
- .replace(/<pre><code/g, '<pre class="code-block"><code')
2796
- .replace(/<code>/g, '<code class="inline-code">')
2797
- .replace(/<table>/g, '<table class="markdown-table">')
2798
- .replace(/<blockquote>/g, '<blockquote class="markdown-quote">');
2799
-
2800
- } catch (error) {
2801
- console.warn('[UIManager] markdown-it parsing failed, falling back to basic formatting:', error);
2802
- // Fall through to basic formatting
2803
- }
2804
- } else {
2805
- }
2806
-
2807
- // Fallback: Enhanced basic markdown-like formatting
2808
-
2809
- // Task lists (checkboxes)
2810
- formattedContent = this.preprocessTaskLists(formattedContent);
2811
-
2812
- // Bold text **text**
2813
- formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2814
-
2815
- // Italic text *text*
2816
- formattedContent = formattedContent.replace(/\*(.*?)\*/g, '<em>$1</em>');
2817
-
2818
- // Code blocks ```code```
2819
- formattedContent = formattedContent.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>');
2820
-
2821
- // Inline code `code`
2822
- formattedContent = formattedContent.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
2823
-
2824
- // Headers
2825
- formattedContent = formattedContent.replace(/^### (.*$)/gm, '<h3>$1</h3>');
2826
- formattedContent = formattedContent.replace(/^## (.*$)/gm, '<h2>$1</h2>');
2827
- formattedContent = formattedContent.replace(/^# (.*$)/gm, '<h1>$1</h1>');
2828
-
2829
- // Lists - regular
2830
- formattedContent = formattedContent.replace(/^- (.*$)/gm, '<li>$1</li>');
2831
- formattedContent = formattedContent.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
2832
-
2833
- // Handle markdown-style local file links first [text](local_path) and [text](file://path)
2834
- const markdownLocalLinkRegex = /\[([^\]]+)\]\(([A-Za-z]:\\[^)]+\.html?)\)/g;
2835
- const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+\.html?)\)/g;
2836
-
2837
- // Handle [text](local_path) format - Enhanced path detection and conversion
2838
- formattedContent = formattedContent.replace(markdownLocalLinkRegex, (match, linkText, filePath) => {
2839
- try {
2840
- // Normalize path separators and handle absolute paths
2841
- let normalizedPath = filePath.replace(/\\/g, '/');
2842
-
2843
- // Ensure proper file:// URL format
2844
- let fileUrl;
2845
- if (normalizedPath.startsWith('/')) {
2846
- // Unix-style absolute path
2847
- fileUrl = `file://${normalizedPath}`;
2848
- } else if (normalizedPath.match(/^[A-Za-z]:/)) {
2849
- // Windows-style absolute path (C:, D:, etc.)
2850
- fileUrl = `file:///${normalizedPath}`;
2851
- } else {
2852
- // Relative path - make it absolute based on workspace
2853
- fileUrl = `file:///${normalizedPath}`;
2854
- }
2855
-
2856
-
2857
- return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file: ${filePath}">${linkText}</a>`;
2858
- } catch (error) {
2859
- console.error('[UIManager] Error converting markdown local link:', error);
2860
- return match;
2861
- }
2862
- });
2863
-
2864
- // Handle [text](file://path) format
2865
- formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2866
- try {
2867
- // Decode and fix the file URL
2868
- let decodedUrl = decodeURIComponent(fileUrl);
2869
- let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2870
- cleanPath = cleanPath.replace(/\\/g, '/');
2871
-
2872
- // Recreate proper file URL
2873
- let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2874
- `file:///${cleanPath}` :
2875
- `file://${cleanPath}`;
2876
-
2877
-
2878
- return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2879
- } catch (error) {
2880
- console.error('[UIManager] Error processing markdown file:// link:', error);
2881
- return match;
2882
- }
2883
- });
2884
-
2885
-
2886
- // Handle [text](file://path) format
2887
- formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2888
- try {
2889
- // Decode and fix the file URL
2890
- let decodedUrl = decodeURIComponent(fileUrl);
2891
- let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2892
- cleanPath = cleanPath.replace(/\\/g, '/');
2893
-
2894
- // Recreate proper file URL
2895
- let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2896
- `file:///${cleanPath}` :
2897
- `file://${cleanPath}`;
2898
-
2899
-
2900
- return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2901
- } catch (error) {
2902
- console.error('[UIManager] Error processing markdown file:// link:', error);
2903
- return match;
2904
- }
2905
- });
2906
-
2907
- // Convert URLs to links - Enhanced for file:// protocol handling
2908
- const httpUrlRegex = /(https?:\/\/[^\s]+)/g;
2909
- const fileUrlRegex = /(file:\/\/[^\s]+)/g;
2910
-
2911
- // Handle HTTP/HTTPS URLs normally
2912
- formattedContent = formattedContent.replace(httpUrlRegex, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
2913
-
2914
- // Handle file:// URLs with custom class for special handling
2915
- formattedContent = formattedContent.replace(fileUrlRegex, (match, fileUrl) => {
2916
- try {
2917
- // Immediately decode and fix the file URL
2918
- let decodedUrl = decodeURIComponent(fileUrl);
2919
-
2920
- // Remove file:// protocol and normalize path
2921
- let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2922
- cleanPath = cleanPath.replace(/\\/g, '/');
710
+ // For fast-completing tasks, ensure minimum visibility duration
711
+ this.ensureMinimumControlPanelVisibility();
712
+ break;
2923
713
 
2924
- // Recreate proper file URL
2925
- let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2926
- `file:///${cleanPath}` :
2927
- `file://${cleanPath}`;
714
+ case 'paused':
715
+ console.log('[UIManager] Setting control panel to paused (showing resume/terminate buttons)');
716
+ panel.classList.remove('hidden');
717
+ panel.classList.remove('error-state');
718
+ cancelBtn?.classList.add('hidden');
719
+ resumeBtn?.classList.remove('hidden');
720
+ terminateBtn?.classList.remove('hidden');
721
+ break;
2928
722
 
723
+ case 'error':
724
+ console.log('[UIManager] Setting control panel to error (keeping cancel/terminate buttons visible)');
725
+ panel.classList.remove('hidden');
726
+ panel.classList.add('error-state');
727
+ cancelBtn?.classList.remove('hidden');
728
+ resumeBtn?.classList.add('hidden');
729
+ terminateBtn?.classList.remove('hidden');
730
+ break;
2929
731
 
2930
- return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${fixedUrl}</a>`;
2931
- } catch (error) {
2932
- console.error('[UIManager] Error processing file URL in markdown:', error);
2933
- return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file">${fileUrl}</a>`;
2934
- }
2935
- });
2936
-
2937
-
2938
- // Convert newlines to br tags
2939
- formattedContent = formattedContent.replace(/\n/g, '<br>');
2940
-
2941
- return formattedContent;
2942
- }
2943
-
2944
- preprocessTaskLists(content) {
2945
- // Convert markdown task lists to HTML checkboxes
2946
- // - [ ] unchecked task -> checkbox unchecked
2947
- // - [x] checked task -> checkbox checked
2948
- // - [X] checked task -> checkbox checked
2949
- return content
2950
- .replace(/^- \[ \] (.*)$/gm, '<div class="task-item"><input type="checkbox" disabled> $1</div>')
2951
- .replace(/^- \[[xX]\] (.*)$/gm, '<div class="task-item"><input type="checkbox" checked disabled> $1</div>');
2952
- }
2953
-
2954
- scrollActivityToBottom() {
2955
- if (this.elements.activityLog) {
2956
- this.elements.activityLog.scrollTop = this.elements.activityLog.scrollHeight;
2957
- }
2958
- }
2959
-
2960
- clearTaskInput() {
2961
- if (this.elements.taskInput) {
2962
- this.elements.taskInput.value = '';
2963
- }
2964
-
2965
- if (this.elements.sendBtn) {
2966
- this.elements.sendBtn.disabled = true;
2967
- }
2968
- }
2969
-
2970
- // Modal Management
2971
- displayHistoryModal(sessions) {
2972
- if (!this.elements.historyList) return;
2973
-
2974
- this.elements.historyList.innerHTML = '';
2975
-
2976
- if (sessions.length === 0) {
2977
- this.elements.historyList.innerHTML = `
2978
- <div class="empty-state">
2979
- <div class="empty-state-icon">📝</div>
2980
- <div class="empty-state-title">No Sessions Found</div>
2981
- <div class="empty-state-description">Create a new session to get started.</div>
2982
- </div>
2983
- `;
2984
- } else {
2985
- sessions.forEach(session => {
2986
- const item = this.createHistoryItem(session);
2987
- this.elements.historyList.appendChild(item);
2988
- });
2989
- }
2990
-
2991
- this.openModal('history');
2992
- }
2993
-
2994
- createHistoryItem(session) {
2995
- const item = document.createElement('div');
2996
- item.className = 'history-item';
2997
-
2998
- const createdAt = new Date(session.createdAt || session.lastUpdated).toLocaleString();
2999
- const taskCount = session.taskHistory?.length || 0;
3000
-
3001
- item.innerHTML = `
3002
- <div class="history-item-header">
3003
- <span class="history-session-id">${session.sessionId}</span>
3004
- <span class="history-timestamp">${createdAt}</span>
3005
- </div>
3006
- <div class="history-task">${taskCount} task(s)</div>
3007
- <div class="history-status">
3008
- <span class="status-dot ${session.status || 'active'}"></span>
3009
- ${session.status || 'active'}
3010
- </div>
3011
- `;
3012
-
3013
- item.addEventListener('click', () => {
3014
- this.loadSession(session.sessionId);
3015
- this.closeModal();
3016
- });
3017
-
3018
- return item;
3019
- }
3020
-
3021
- async loadSession(sessionId) {
3022
- // Check if task is running
3023
- if (this.state.isTaskRunning) {
3024
- const canProceed = await this.showTaskRunningWarning('load a different session');
3025
- if (!canProceed) return;
3026
- }
3027
-
3028
- try {
3029
- this.showLoading('Loading session...');
3030
-
3031
- const success = await this.sessionManager.loadSession(sessionId);
3032
-
3033
- this.hideLoading();
3034
-
3035
- if (!success) {
3036
- this.showNotification('Failed to load session', 'error');
3037
- }
3038
- } catch (error) {
3039
- this.hideLoading();
3040
- this.showNotification(`Failed to load session: ${error.message}`, 'error');
3041
- }
3042
- }
3043
-
3044
- async displaySettingsModal() {
3045
- // Load current backend URL from API client
3046
- if (this.elements.backendUrl && this.apiClient) {
3047
- this.elements.backendUrl.value = this.apiClient.baseURL;
3048
- }
3049
-
3050
- this.openModal('settings');
3051
- }
3052
-
3053
-
3054
- updateLLMProfileSelect() {
3055
- if (!this.elements.llmProfileSelect) return;
3056
-
3057
- const select = this.elements.llmProfileSelect;
3058
- select.innerHTML = ''; // Remove default option
3059
-
3060
- if (this.state.llmProfiles.length === 0) {
3061
- // Add placeholder option when no profiles available
3062
- const placeholderOption = document.createElement('option');
3063
- placeholderOption.value = '';
3064
- placeholderOption.textContent = 'No LLM profiles configured - Go to Settings';
3065
- placeholderOption.disabled = true;
3066
- placeholderOption.selected = true;
3067
- select.appendChild(placeholderOption);
3068
- } else {
3069
- // Add default empty option
3070
- const emptyOption = document.createElement('option');
3071
- emptyOption.value = '';
3072
- emptyOption.textContent = 'Select LLM Profile...';
3073
- emptyOption.disabled = true;
3074
- select.appendChild(emptyOption);
3075
-
3076
- // Add actual profiles
3077
- this.state.llmProfiles.forEach(profile => {
3078
- const option = document.createElement('option');
3079
- option.value = profile.profile_name;
3080
- option.textContent = profile.profile_name;
3081
- if (profile.is_default) {
3082
- option.selected = true;
3083
- }
3084
- select.appendChild(option);
3085
- });
3086
- }
3087
-
3088
- // Update send button state if taskInput exists
3089
- if (this.elements.taskInput) {
3090
- this.handleTaskInputChange({ target: this.elements.taskInput });
732
+ default:
733
+ console.log(`[UIManager] Unknown control panel status: ${status}, hiding panel`);
734
+ panel.classList.add('hidden');
735
+ panel.classList.remove('error-state');
3091
736
  }
3092
737
  }
3093
-
3094
- showLLMProfileRequiredModal(action) {
3095
- const isConfigureAction = action === 'configure';
3096
- const title = isConfigureAction ? 'LLM Profile Required' : 'Please Select LLM Profile';
3097
- const message = isConfigureAction
3098
- ? 'No LLM profiles are configured. You need to configure at least one LLM profile before sending tasks.'
3099
- : 'Please select an LLM profile from the dropdown to proceed with your task.';
3100
-
3101
- const modal = this.createWarningModal({
3102
- title,
3103
- message,
3104
- details: isConfigureAction
3105
- ? 'LLM profiles contain the configuration for AI models (like OpenAI, Claude, etc.) that will process your tasks.'
3106
- : null,
3107
- buttons: isConfigureAction
3108
- ? [
3109
- {
3110
- text: 'Open Settings',
3111
- style: 'primary',
3112
- action: () => {
3113
- this.closeWarningModal();
3114
- this.handleShowSettings();
3115
- }
3116
- },
3117
- {
3118
- text: 'Cancel',
3119
- style: 'secondary',
3120
- action: () => {
3121
- this.closeWarningModal();
3122
- }
3123
- }
3124
- ]
3125
- : [
3126
- {
3127
- text: 'Open Settings',
3128
- style: 'secondary',
3129
- action: () => {
3130
- this.closeWarningModal();
3131
- this.handleShowSettings();
3132
- }
3133
- },
3134
- {
3135
- text: 'OK',
3136
- style: 'primary',
3137
- action: () => {
3138
- this.closeWarningModal();
3139
- this.elements.llmProfileSelect?.focus();
3140
- }
3141
- }
3142
- ]
3143
- });
738
+
739
+ ensureMinimumControlPanelVisibility() {
740
+ // Set a flag to prevent immediate hiding of control panel for fast tasks
741
+ this.controlPanelMinVisibilityActive = true;
3144
742
 
3145
- document.body.appendChild(modal);
743
+ // Clear the flag after minimum visibility period (2 seconds)
744
+ setTimeout(() => {
745
+ this.controlPanelMinVisibilityActive = false;
746
+ console.log('[UIManager] Minimum control panel visibility period ended');
747
+ }, 2000);
3146
748
  }
3147
749
 
3148
- updateSettingsDisplay() {
3149
- // Update LLM profiles list in settings
3150
- if (this.elements.llmProfilesList) {
3151
- this.elements.llmProfilesList.innerHTML = '';
3152
-
3153
- this.state.llmProfiles.forEach(profile => {
3154
- const item = this.createProfileItem(profile, 'llm');
3155
- this.elements.llmProfilesList.appendChild(item);
3156
- });
750
+ clearActivityLog() {
751
+ if (this.elements.activityLog) {
752
+ this.elements.activityLog.innerHTML = '';
3157
753
  }
3158
-
3159
- // Update MCP profiles list in settings
3160
- if (this.elements.mcpFilesList) {
3161
- this.elements.mcpFilesList.innerHTML = '';
3162
-
3163
- this.state.mcpProfiles.forEach(profile => {
3164
- const item = this.createProfileItem(profile, 'mcp');
3165
- this.elements.mcpFilesList.appendChild(item);
3166
- });
754
+ }
755
+
756
+ clearWelcomeMessage() {
757
+ if (this.elements.activityLog) {
758
+ const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
759
+ if (welcomeMsg) {
760
+ welcomeMsg.remove();
761
+ }
3167
762
  }
3168
763
  }
3169
764
 
3170
- createProfileItem(profile, type) {
3171
- const item = document.createElement('div');
3172
- item.className = 'profile-item';
3173
-
3174
- const isLLM = type === 'llm';
3175
- const name = profile.profile_name;
3176
- const details = isLLM ?
3177
- `${profile.provider} - ${profile.model}` :
3178
- `${profile.server_name || 'Unknown'}`;
3179
-
3180
- // Add active status
3181
- const activeStatus = profile.is_active ? 'Active' : 'Inactive';
3182
- const activeClass = profile.is_active ? 'active' : 'inactive';
3183
-
3184
- item.innerHTML = `
3185
- <div class="profile-header">
3186
- <span class="profile-name">${name}</span>
3187
- <div class="profile-badges">
3188
- ${isLLM && profile.is_default ? '<span class="profile-default">Default</span>' : ''}
3189
- <span class="profile-status ${activeClass}">${activeStatus}</span>
765
+ showWelcomeMessage() {
766
+ const welcomeHTML = `
767
+ <div class="welcome-message">
768
+ <div class="welcome-text">
769
+ <h4>Welcome to VibeSurf</h4>
770
+ <p>Let's vibe surfing the world with AI automation</p>
3190
771
  </div>
3191
- <div class="profile-actions">
3192
- <button class="profile-btn edit-btn" data-name="${name}" data-type="${type}">Edit</button>
3193
- <button class="profile-btn danger delete-btn" data-name="${name}" data-type="${type}">Delete</button>
772
+ <div class="quick-tasks">
773
+ <div class="task-suggestion" data-task="research">
774
+ <div class="task-icon">🔍</div>
775
+ <div class="task-content">
776
+ <div class="task-title">Research Founders</div>
777
+ <div class="task-description">Search information about browser-use and browser-use-webui, write a brief report</div>
778
+ </div>
779
+ </div>
780
+ <div class="task-suggestion" data-task="news">
781
+ <div class="task-icon">📰</div>
782
+ <div class="task-content">
783
+ <div class="task-title">HackerNews Summary</div>
784
+ <div class="task-description">Get top 10 news from HackerNews and provide a summary</div>
785
+ </div>
786
+ </div>
787
+ <div class="task-suggestion" data-task="analysis">
788
+ <div class="task-icon">📈</div>
789
+ <div class="task-content">
790
+ <div class="task-title">Stock Market Analysis</div>
791
+ <div class="task-description">Analyze recent week stock market trends for major tech companies</div>
792
+ </div>
793
+ </div>
3194
794
  </div>
3195
795
  </div>
3196
- <div class="profile-details">${details}</div>
3197
796
  `;
3198
797
 
3199
- // Add event listeners
3200
- const editBtn = item.querySelector('.edit-btn');
3201
- const deleteBtn = item.querySelector('.delete-btn');
3202
-
3203
- editBtn?.addEventListener('click', () => {
3204
- this.editProfile(name, type);
3205
- });
798
+ if (this.elements.activityLog) {
799
+ this.elements.activityLog.innerHTML = welcomeHTML;
800
+ this.bindTaskSuggestionEvents();
801
+ }
802
+ }
803
+
804
+ bindTaskSuggestionEvents() {
805
+ const taskSuggestions = document.querySelectorAll('.task-suggestion');
3206
806
 
3207
- deleteBtn?.addEventListener('click', () => {
3208
- this.deleteProfile(name, type);
807
+ taskSuggestions.forEach(suggestion => {
808
+ suggestion.addEventListener('click', () => {
809
+ const taskDescription = suggestion.querySelector('.task-description').textContent;
810
+
811
+ // Populate task input with suggestion (only description, no title prefix)
812
+ if (this.elements.taskInput) {
813
+ this.elements.taskInput.value = taskDescription;
814
+ this.elements.taskInput.focus();
815
+
816
+ // Trigger input change event for validation and auto-resize
817
+ this.handleTaskInputChange({ target: this.elements.taskInput });
818
+
819
+ // Auto-send the task
820
+ setTimeout(() => {
821
+ this.handleSendTask();
822
+ }, 100); // Small delay to ensure input processing is complete
823
+ }
824
+ });
3209
825
  });
3210
-
3211
- return item;
3212
826
  }
3213
827
 
3214
- async editProfile(name, type) {
3215
- this.showNotification(`Edit ${type.toUpperCase()} profile '${name}' coming soon...`, 'info');
3216
- // TODO: Implement profile editing form
3217
- console.log(`[UIManager] Edit ${type} profile:`, name);
828
+ displayActivityLogs(logs) {
829
+ this.clearActivityLog();
830
+
831
+ if (logs.length === 0) {
832
+ this.showWelcomeMessage();
833
+ return;
834
+ }
835
+
836
+ logs.forEach(log => this.addActivityLog(log));
837
+ this.scrollActivityToBottom();
3218
838
  }
3219
839
 
3220
- async deleteProfile(name, type) {
3221
- if (!confirm(`Are you sure you want to delete the ${type.toUpperCase()} profile '${name}'?`)) {
840
+ addActivityLog(activityData) {
841
+ // Filter out "done" status messages from UI display only
842
+ const agentStatus = activityData.agent_status || activityData.status || '';
843
+
844
+ // Filter done messages that are just status updates without meaningful content
845
+ if (agentStatus.toLowerCase() === 'done') {
3222
846
  return;
3223
847
  }
3224
848
 
3225
- try {
3226
- this.showLoading(`Deleting ${type} profile...`);
3227
-
3228
- if (type === 'llm') {
3229
- await this.apiClient.deleteLLMProfile(name);
3230
- } else {
3231
- await this.apiClient.deleteMCPProfile(name);
849
+ const activityItem = this.createActivityItem(activityData);
850
+
851
+ if (this.elements.activityLog) {
852
+ // Remove welcome message if present
853
+ const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
854
+ if (welcomeMsg) {
855
+ welcomeMsg.remove();
3232
856
  }
3233
857
 
3234
- // Refresh the settings data
3235
- await this.loadSettingsData();
858
+ this.elements.activityLog.appendChild(activityItem);
859
+ activityItem.classList.add('fade-in');
3236
860
 
3237
- this.hideLoading();
3238
- this.showNotification(`${type.toUpperCase()} profile '${name}' deleted successfully`, 'success');
3239
- } catch (error) {
3240
- this.hideLoading();
3241
- this.showNotification(`Failed to delete ${type} profile: ${error.message}`, 'error');
3242
- }
3243
- }
3244
-
3245
- openModal(modalName) {
3246
- const modal = document.getElementById(`${modalName}-modal`);
3247
- if (modal) {
3248
- modal.classList.remove('hidden');
3249
- modal.classList.add('scale-in');
3250
- this.state.currentModal = modalName;
3251
- }
3252
- }
3253
-
3254
- closeModal() {
3255
- if (this.state.currentModal) {
3256
- const modal = document.getElementById(`${this.state.currentModal}-modal`);
3257
- if (modal) {
3258
- modal.classList.add('hidden');
3259
- modal.classList.remove('scale-in');
3260
- }
3261
- this.state.currentModal = null;
861
+ // Bind copy button functionality
862
+ this.bindCopyButtonEvent(activityItem, activityData);
3262
863
  }
3263
864
  }
3264
865
 
3265
- // History Modal Handlers
3266
- setupHistoryModalHandlers() {
3267
- // View More Tasks button
3268
- this.elements.viewMoreTasksBtn?.addEventListener('click', this.handleViewMoreTasks.bind(this));
866
+ createActivityItem(data) {
867
+ const item = document.createElement('div');
868
+
869
+ // Extract activity data with correct keys
870
+ const agentName = data.agent_name || 'system';
871
+ const agentStatus = data.agent_status || data.status || 'info';
872
+ const agentMsg = data.agent_msg || data.message || data.action_description || 'No description';
873
+ const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString();
3269
874
 
3270
- // Back to Recent button
3271
- this.elements.backToRecentBtn?.addEventListener('click', this.handleBackToRecent.bind(this));
875
+ // Determine if this is a user message (should be on the right)
876
+ const isUser = agentName.toLowerCase() === 'user';
3272
877
 
3273
- // Search and filter
3274
- this.elements.sessionSearch?.addEventListener('input', this.handleSessionSearch.bind(this));
3275
- this.elements.sessionFilter?.addEventListener('change', this.handleSessionFilter.bind(this));
878
+ // Set CSS classes based on agent type and status
879
+ item.className = `activity-item ${isUser ? 'user-message' : 'agent-message'} ${agentStatus}`;
3276
880
 
3277
- // Pagination
3278
- this.elements.prevPageBtn?.addEventListener('click', this.handlePrevPage.bind(this));
3279
- this.elements.nextPageBtn?.addEventListener('click', this.handleNextPage.bind(this));
881
+ // Create the message structure similar to chat interface
882
+ item.innerHTML = `
883
+ <div class="message-container ${isUser ? 'user-container' : 'agent-container'}">
884
+ <div class="message-header">
885
+ <span class="agent-name">${agentName}</span>
886
+ <span class="message-time">${timestamp}</span>
887
+ </div>
888
+ <div class="message-bubble ${isUser ? 'user-bubble' : 'agent-bubble'}">
889
+ <div class="message-status">
890
+ <span class="status-indicator ${agentStatus}">${this.getStatusIcon(agentStatus)}</span>
891
+ <span class="status-text">${agentStatus}</span>
892
+ </div>
893
+ <div class="message-content">
894
+ ${this.formatActivityContent(agentMsg)}
895
+ </div>
896
+ <button class="copy-message-btn" title="Copy message">
897
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
898
+ <path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z" fill="currentColor"/>
899
+ </svg>
900
+ </button>
901
+ </div>
902
+ </div>
903
+ `;
3280
904
 
3281
- console.log('[UIManager] History modal handlers bound');
3282
- }
3283
-
3284
- async handleViewMoreTasks() {
3285
- try {
3286
- console.log('[UIManager] View More Tasks clicked');
3287
- this.showLoading('Loading all sessions...');
3288
-
3289
- // Switch to all sessions view
3290
- this.state.historyMode = 'all';
3291
- console.log('[UIManager] Set history mode to "all"');
3292
-
3293
- await this.loadAllSessions();
3294
- console.log('[UIManager] All sessions loaded, switching view');
3295
-
3296
- this.displayAllSessionsView();
3297
- console.log('[UIManager] All sessions view displayed');
3298
-
3299
- this.hideLoading();
3300
- } catch (error) {
3301
- this.hideLoading();
3302
- console.error('[UIManager] Error in handleViewMoreTasks:', error);
3303
- this.showNotification(`Failed to load sessions: ${error.message}`, 'error');
3304
- }
3305
- }
3306
-
3307
- handleBackToRecent() {
3308
- this.state.historyMode = 'recent';
3309
- this.displayRecentTasksView();
3310
- }
3311
-
3312
- handleSessionSearch(event) {
3313
- this.state.searchQuery = event.target.value.trim().toLowerCase();
3314
- this.filterAndDisplaySessions();
3315
- }
3316
-
3317
- handleSessionFilter(event) {
3318
- this.state.statusFilter = event.target.value;
3319
- this.filterAndDisplaySessions();
3320
- }
3321
-
3322
- handlePrevPage() {
3323
- if (this.state.currentPage > 1) {
3324
- this.state.currentPage--;
3325
- this.filterAndDisplaySessions();
3326
- }
905
+ return item;
3327
906
  }
3328
907
 
3329
- handleNextPage() {
3330
- if (this.state.currentPage < this.state.totalPages) {
3331
- this.state.currentPage++;
3332
- this.filterAndDisplaySessions();
908
+ bindCopyButtonEvent(activityItem, activityData) {
909
+ const copyBtn = activityItem.querySelector('.copy-message-btn');
910
+
911
+ if (copyBtn) {
912
+ copyBtn.addEventListener('click', async (e) => {
913
+ e.preventDefault();
914
+ e.stopPropagation();
915
+
916
+ await this.copyMessageToClipboard(activityData);
917
+ });
3333
918
  }
3334
919
  }
3335
920
 
3336
- // History Data Loading Methods
3337
- async loadRecentTasks() {
921
+ async copyMessageToClipboard(activityData) {
3338
922
  try {
3339
- console.log('[UIManager] Loading recent tasks...');
3340
- const response = await this.apiClient.getRecentTasks();
923
+ // Extract only the message content (no agent info or timestamps)
924
+ const agentMsg = activityData.agent_msg || activityData.message || activityData.action_description || 'No description';
3341
925
 
3342
- // Handle API response structure: { tasks: [...], total_count: ..., limit: ... }
3343
- let tasks = [];
3344
- if (response && response.tasks && Array.isArray(response.tasks)) {
3345
- tasks = response.tasks;
3346
- } else if (response && Array.isArray(response)) {
3347
- tasks = response;
3348
- } else if (response && response.data && Array.isArray(response.data)) {
3349
- tasks = response.data;
926
+ // Convert to plain text
927
+ let messageText = '';
928
+ if (typeof agentMsg === 'object') {
929
+ messageText = JSON.stringify(agentMsg, null, 2);
930
+ } else {
931
+ messageText = String(agentMsg);
932
+ // Strip HTML tags for plain text copy
933
+ messageText = messageText.replace(/<[^>]*>/g, '');
934
+ // Decode HTML entities
935
+ messageText = messageText
936
+ .replace(/&amp;/g, '&')
937
+ .replace(/&lt;/g, '<')
938
+ .replace(/&gt;/g, '>')
939
+ .replace(/&quot;/g, '"')
940
+ .replace(/&#39;/g, "'")
941
+ .replace(/&nbsp;/g, ' ')
942
+ .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
943
+ .replace(/<br\s*\/?>/gi, '\n') // Convert <br> tags to newlines
944
+ .replace(/\s+/g, ' ') // Normalize whitespace
945
+ .trim(); // Remove leading/trailing whitespace
3350
946
  }
3351
947
 
3352
- // Take only the first 3 most recent tasks
3353
- this.state.recentTasks = tasks.slice(0, 3);
3354
- console.log('[UIManager] Recent tasks loaded:', this.state.recentTasks.length);
948
+ // Check clipboard API availability
949
+ console.log('[UIManager] navigator.clipboard available:', !!navigator.clipboard);
950
+ console.log('[UIManager] writeText method available:', !!(navigator.clipboard && navigator.clipboard.writeText));
951
+ console.log('[UIManager] Document has focus:', document.hasFocus());
3355
952
 
3356
- return this.state.recentTasks;
3357
- } catch (error) {
3358
- console.error('[UIManager] Failed to load recent tasks:', error);
3359
- this.state.recentTasks = [];
3360
- throw error;
3361
- }
3362
- }
3363
-
3364
- async loadAllSessions() {
3365
- try {
3366
- console.log('[UIManager] Loading all sessions...');
3367
- const response = await this.apiClient.getAllSessions();
953
+ // Try multiple clipboard methods
954
+ let copySuccess = false;
955
+ let lastError = null;
3368
956
 
3369
- // Handle API response structure: { sessions: [...], total_count: ..., limit: ..., offset: ... }
3370
- let sessions = [];
3371
- if (response && response.sessions && Array.isArray(response.sessions)) {
3372
- sessions = response.sessions;
3373
- } else if (response && Array.isArray(response)) {
3374
- sessions = response;
3375
- } else if (response && response.data && Array.isArray(response.data)) {
3376
- sessions = response.data;
957
+ // Method 1: Chrome extension messaging approach
958
+ try {
959
+ console.log('[UIManager] Trying Chrome extension messaging approach...');
960
+ await new Promise((resolve, reject) => {
961
+ chrome.runtime.sendMessage({
962
+ type: 'COPY_TO_CLIPBOARD',
963
+ text: messageText
964
+ }, (response) => {
965
+ if (chrome.runtime.lastError) {
966
+ reject(new Error(chrome.runtime.lastError.message));
967
+ } else if (response && response.success) {
968
+ resolve();
969
+ } else {
970
+ reject(new Error('Extension messaging copy failed'));
971
+ }
972
+ });
973
+ });
974
+ copySuccess = true;
975
+ console.log('[UIManager] Copied using Chrome extension messaging');
976
+ } catch (extensionError) {
977
+ console.warn('[UIManager] Chrome extension messaging failed:', extensionError);
978
+ lastError = extensionError;
3377
979
  }
3378
980
 
3379
- this.state.allSessions = sessions;
3380
- console.log('[UIManager] All sessions loaded:', this.state.allSessions.length);
3381
-
3382
- return this.state.allSessions;
3383
- } catch (error) {
3384
- console.error('[UIManager] Failed to load all sessions:', error);
3385
- this.state.allSessions = [];
3386
- throw error;
3387
- }
3388
- }
3389
-
3390
- // History Display Methods
3391
- displayHistoryModal() {
3392
- if (this.state.historyMode === 'recent') {
3393
- this.displayRecentTasksView();
3394
- } else {
3395
- this.displayAllSessionsView();
3396
- }
3397
- this.openModal('history');
3398
- }
3399
-
3400
- displayRecentTasksView() {
3401
- console.log('[UIManager] Switching to recent tasks view');
3402
-
3403
- // Show recent tasks section and hide all sessions section
3404
- if (this.elements.recentTasksList && this.elements.allSessionsSection) {
3405
- const recentParent = this.elements.recentTasksList.parentElement;
3406
- if (recentParent) {
3407
- recentParent.classList.remove('hidden');
3408
- recentParent.style.display = 'block';
3409
- console.log('[UIManager] Showed recent tasks section');
3410
- }
3411
- this.elements.allSessionsSection.classList.add('hidden');
3412
- this.elements.allSessionsSection.style.display = 'none';
3413
- console.log('[UIManager] Hidden all sessions section');
3414
- }
3415
-
3416
- this.renderRecentTasks();
3417
- }
3418
-
3419
- displayAllSessionsView() {
3420
- console.log('[UIManager] Switching to all sessions view');
3421
- console.log('[UIManager] Elements check:', {
3422
- recentTasksList: !!this.elements.recentTasksList,
3423
- allSessionsSection: !!this.elements.allSessionsSection,
3424
- recentTasksParent: !!this.elements.recentTasksList?.parentElement
3425
- });
3426
-
3427
- // Hide recent tasks section and show all sessions section
3428
- if (this.elements.recentTasksList && this.elements.allSessionsSection) {
3429
- const recentParent = this.elements.recentTasksList.parentElement;
3430
- if (recentParent) {
3431
- recentParent.style.display = 'none';
3432
- recentParent.classList.add('hidden');
3433
- console.log('[UIManager] Hidden recent tasks section');
981
+ // Method 2: Modern clipboard API (if extension method failed)
982
+ if (!copySuccess && navigator.clipboard && navigator.clipboard.writeText) {
983
+ try {
984
+ console.log('[UIManager] Trying modern clipboard API...');
985
+ await navigator.clipboard.writeText(messageText);
986
+ copySuccess = true;
987
+ console.log('[UIManager] Copied using modern clipboard API');
988
+ } catch (clipboardError) {
989
+ console.warn('[UIManager] Modern clipboard API failed:', clipboardError);
990
+ lastError = clipboardError;
991
+ }
3434
992
  }
3435
993
 
3436
- // Remove hidden class and set display block
3437
- this.elements.allSessionsSection.classList.remove('hidden');
3438
- this.elements.allSessionsSection.style.display = 'block';
3439
- console.log('[UIManager] Showed all sessions section - removed hidden class and set display block');
994
+ // Method 3: Fallback using execCommand
995
+ if (!copySuccess) {
996
+ try {
997
+ console.log('[UIManager] Trying execCommand fallback...');
998
+ const textArea = document.createElement('textarea');
999
+ textArea.value = messageText;
1000
+ textArea.style.position = 'fixed';
1001
+ textArea.style.left = '-999999px';
1002
+ textArea.style.top = '-999999px';
1003
+ textArea.style.opacity = '0';
1004
+ document.body.appendChild(textArea);
1005
+ textArea.focus();
1006
+ textArea.select();
1007
+ textArea.setSelectionRange(0, textArea.value.length);
1008
+
1009
+ const success = document.execCommand('copy');
1010
+ document.body.removeChild(textArea);
1011
+
1012
+ if (success) {
1013
+ copySuccess = true;
1014
+ console.log('[UIManager] Copied using execCommand fallback');
1015
+ } else {
1016
+ console.warn('[UIManager] execCommand returned false');
1017
+ }
1018
+ } catch (execError) {
1019
+ console.warn('[UIManager] execCommand fallback failed:', execError);
1020
+ lastError = execError;
1021
+ }
1022
+ }
3440
1023
 
3441
- // Debug: Check computed styles
3442
- const computedStyle = window.getComputedStyle(this.elements.allSessionsSection);
3443
- console.log('[UIManager] All sessions section computed display:', computedStyle.display);
3444
- console.log('[UIManager] All sessions section classList:', this.elements.allSessionsSection.classList.toString());
1024
+ if (copySuccess) {
1025
+ // Show visual feedback
1026
+ this.showCopyFeedback();
1027
+ console.log('[UIManager] Copy operation completed successfully');
1028
+ } else {
1029
+ throw new Error(`All clipboard methods failed. Last error: ${lastError?.message || 'Unknown error'}`);
1030
+ }
3445
1031
 
3446
- } else {
3447
- console.error('[UIManager] Missing elements for view switching:', {
3448
- recentTasksList: !!this.elements.recentTasksList,
3449
- allSessionsSection: !!this.elements.allSessionsSection
3450
- });
3451
- }
3452
-
3453
- // Reset search and filter
3454
- this.state.currentPage = 1;
3455
- this.state.searchQuery = '';
3456
- this.state.statusFilter = 'all';
3457
-
3458
- if (this.elements.sessionSearch) {
3459
- this.elements.sessionSearch.value = '';
3460
- }
3461
- if (this.elements.sessionFilter) {
3462
- this.elements.sessionFilter.value = 'all';
1032
+ } catch (error) {
1033
+ console.error('[UIManager] Failed to copy message:', error);
1034
+ this.showNotification('Copy Fail: ' + error.message, 'error');
3463
1035
  }
3464
-
3465
- console.log('[UIManager] About to filter and display sessions');
3466
- this.filterAndDisplaySessions();
3467
1036
  }
3468
1037
 
3469
- renderRecentTasks() {
3470
- if (!this.elements.recentTasksList) return;
3471
-
3472
- this.elements.recentTasksList.innerHTML = '';
3473
-
3474
- if (this.state.recentTasks.length === 0) {
3475
- this.elements.recentTasksList.innerHTML = `
3476
- <div class="empty-state">
3477
- <div class="empty-state-icon">📝</div>
3478
- <div class="empty-state-title">No Recent Tasks</div>
3479
- <div class="empty-state-description">Start a new task to see it here.</div>
3480
- </div>
3481
- `;
3482
- return;
3483
- }
3484
-
3485
- this.state.recentTasks.forEach(task => {
3486
- const taskItem = this.createTaskItem(task);
3487
- this.elements.recentTasksList.appendChild(taskItem);
3488
- });
1038
+ showCopyFeedback() {
1039
+ // Show a brief success notification
1040
+ this.showNotification('Message copied to clipboard!', 'success');
3489
1041
  }
3490
1042
 
3491
- filterAndDisplaySessions() {
3492
- if (!this.elements.allSessionsList) {
3493
- console.error('[UIManager] allSessionsList element not found');
3494
- return;
3495
- }
3496
-
3497
- console.log('[UIManager] Filtering sessions. Total sessions:', this.state.allSessions.length);
3498
- console.log('[UIManager] Search query:', this.state.searchQuery);
3499
- console.log('[UIManager] Status filter:', this.state.statusFilter);
3500
-
3501
- let filteredSessions = [...this.state.allSessions]; // Create copy
3502
-
3503
- // Apply search filter
3504
- if (this.state.searchQuery) {
3505
- filteredSessions = filteredSessions.filter(session =>
3506
- session.session_id.toLowerCase().includes(this.state.searchQuery) ||
3507
- (session.description && session.description.toLowerCase().includes(this.state.searchQuery))
3508
- );
3509
- }
3510
-
3511
- // Apply status filter
3512
- if (this.state.statusFilter !== 'all') {
3513
- filteredSessions = filteredSessions.filter(session =>
3514
- (session.status || 'active').toLowerCase() === this.state.statusFilter.toLowerCase()
3515
- );
3516
- }
3517
-
3518
- console.log('[UIManager] Filtered sessions count:', filteredSessions.length);
3519
-
3520
- // Calculate pagination
3521
- const totalSessions = filteredSessions.length;
3522
- this.state.totalPages = Math.ceil(totalSessions / this.state.pageSize);
3523
-
3524
- // Ensure current page is valid
3525
- if (this.state.currentPage > this.state.totalPages) {
3526
- this.state.currentPage = Math.max(1, this.state.totalPages);
1043
+ getStatusIcon(status) {
1044
+ switch (status.toLowerCase()) {
1045
+ case 'working':
1046
+ return '⚙️';
1047
+ case 'thinking':
1048
+ return '🤔';
1049
+ case 'result':
1050
+ case 'done':
1051
+ return '✅';
1052
+ case 'error':
1053
+ return '❌';
1054
+ case 'paused':
1055
+ return '⏸️';
1056
+ case 'running':
1057
+ return '🔄';
1058
+ case 'request':
1059
+ return '💡';
1060
+ default:
1061
+ return '💡';
3527
1062
  }
3528
-
3529
- // Get sessions for current page
3530
- const startIndex = (this.state.currentPage - 1) * this.state.pageSize;
3531
- const endIndex = startIndex + this.state.pageSize;
3532
- const paginatedSessions = filteredSessions.slice(startIndex, endIndex);
3533
-
3534
- console.log('[UIManager] Paginated sessions for display:', paginatedSessions.length);
3535
-
3536
- // Render sessions
3537
- this.renderSessionsList(paginatedSessions);
3538
-
3539
- // Update pagination controls
3540
- this.updatePaginationControls();
3541
1063
  }
3542
1064
 
3543
- renderSessionsList(sessions) {
3544
- if (!this.elements.allSessionsList) {
3545
- console.error('[UIManager] allSessionsList element not found for rendering');
3546
- return;
3547
- }
3548
-
3549
- console.log('[UIManager] Rendering sessions list with', sessions.length, 'sessions');
3550
-
3551
- this.elements.allSessionsList.innerHTML = '';
3552
-
3553
- if (sessions.length === 0) {
3554
- console.log('[UIManager] No sessions to display, showing empty state');
3555
- this.elements.allSessionsList.innerHTML = `
3556
- <div class="empty-state">
3557
- <div class="empty-state-icon">🔍</div>
3558
- <div class="empty-state-title">No Sessions Found</div>
3559
- <div class="empty-state-description">Try adjusting your search or filter criteria.</div>
3560
- </div>
3561
- `;
3562
- return;
3563
- }
3564
-
3565
- sessions.forEach((session, index) => {
3566
- console.log(`[UIManager] Creating session item ${index + 1}:`, session.session_id);
3567
- const sessionItem = this.createSessionItem(session);
3568
- this.elements.allSessionsList.appendChild(sessionItem);
3569
- });
1065
+ formatActivityContent(content) {
1066
+ if (!content) return 'No content';
3570
1067
 
3571
- console.log('[UIManager] Sessions list rendered successfully');
3572
- }
3573
-
3574
- updatePaginationControls() {
3575
- // Update pagination buttons
3576
- if (this.elements.prevPageBtn) {
3577
- this.elements.prevPageBtn.disabled = this.state.currentPage <= 1;
1068
+ // Handle object content
1069
+ if (typeof content === 'object') {
1070
+ return `<pre class="json-content">${JSON.stringify(content, null, 2)}</pre>`;
3578
1071
  }
3579
1072
 
3580
- if (this.elements.nextPageBtn) {
3581
- this.elements.nextPageBtn.disabled = this.state.currentPage >= this.state.totalPages;
3582
- }
1073
+ // Convert content to string
1074
+ let formattedContent = String(content);
3583
1075
 
3584
- // Update page info
3585
- if (this.elements.pageInfo) {
3586
- if (this.state.totalPages === 0) {
3587
- this.elements.pageInfo.textContent = 'No results';
3588
- } else {
3589
- this.elements.pageInfo.textContent = `Page ${this.state.currentPage} of ${this.state.totalPages}`;
1076
+ // Use markdown-it library for proper markdown rendering if available
1077
+ if (typeof markdownit !== 'undefined') {
1078
+ try {
1079
+ // Create markdown-it instance with enhanced options
1080
+ const md = markdownit({
1081
+ html: true, // Enable HTML tags in source
1082
+ breaks: true, // Convert '\n' in paragraphs into <br>
1083
+ linkify: true, // Autoconvert URL-like text to links
1084
+ typographer: true // Enable some language-neutral replacement + quotes beautification
1085
+ });
1086
+
1087
+ // Override link renderer to handle file:// protocol
1088
+ const defaultLinkRenderer = md.renderer.rules.link_open || function(tokens, idx, options, env, renderer) {
1089
+ return renderer.renderToken(tokens, idx, options);
1090
+ };
1091
+
1092
+ md.renderer.rules.link_open = function (tokens, idx, options, env, renderer) {
1093
+ const token = tokens[idx];
1094
+ const href = token.attrGet('href');
1095
+
1096
+ if (href && href.startsWith('file://')) {
1097
+ // Convert file:// links to our custom file-link format
1098
+ token.attrSet('href', '#');
1099
+ token.attrSet('class', 'file-link');
1100
+ token.attrSet('data-file-path', href);
1101
+ token.attrSet('title', `Click to open file: ${href}`);
1102
+ }
1103
+
1104
+ return defaultLinkRenderer(tokens, idx, options, env, renderer);
1105
+ };
1106
+
1107
+ // Add task list support manually (markdown-it doesn't have built-in task lists)
1108
+ formattedContent = this.preprocessTaskLists(formattedContent);
1109
+
1110
+ // Pre-process file:// markdown links since markdown-it doesn't recognize them
1111
+ const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+)\)/g;
1112
+ formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
1113
+ // Convert to HTML format that markdown-it will preserve
1114
+ return `<a href="${fileUrl}" class="file-link-markdown">${linkText}</a>`;
1115
+ });
1116
+
1117
+ // Parse markdown
1118
+ const htmlContent = md.render(formattedContent);
1119
+
1120
+ // Post-process to handle local file path links
1121
+ let processedContent = htmlContent;
1122
+
1123
+ // Convert our pre-processed file:// links to proper file-link format
1124
+ const preProcessedFileLinkRegex = /<a href="(file:\/\/[^"]+)"[^>]*class="file-link-markdown"[^>]*>([^<]*)<\/a>/g;
1125
+ processedContent = processedContent.replace(preProcessedFileLinkRegex, (match, fileUrl, linkText) => {
1126
+ try {
1127
+ // Keep original URL but properly escape HTML attributes
1128
+ // Don't decode/encode to preserve original spaces and special characters
1129
+ const escapedUrl = fileUrl.replace(/"/g, '&quot;');
1130
+
1131
+ return `<a href="#" class="file-link" data-file-path="${escapedUrl}" title="Click to open file: ${linkText}">${linkText}</a>`;
1132
+ } catch (error) {
1133
+ console.error('[UIManager] Error processing pre-processed file:// link:', error);
1134
+ return match;
1135
+ }
1136
+ });
1137
+
1138
+ // Add custom classes and post-process
1139
+ return processedContent
1140
+ .replace(/<pre><code/g, '<pre class="code-block"><code')
1141
+ .replace(/<code>/g, '<code class="inline-code">')
1142
+ .replace(/<table>/g, '<table class="markdown-table">')
1143
+ .replace(/<blockquote>/g, '<blockquote class="markdown-quote">');
1144
+
1145
+ } catch (error) {
1146
+ console.warn('[UIManager] markdown-it parsing failed, falling back to basic formatting:', error);
1147
+ // Fall through to basic formatting
3590
1148
  }
3591
1149
  }
3592
- }
3593
-
3594
- // Item Creation Methods
3595
- createTaskItem(task) {
3596
- const item = document.createElement('div');
3597
- item.className = 'recent-task-item';
3598
1150
 
3599
- const sessionId = task.session_id || 'Unknown';
3600
- const taskDesc = task.description || task.task_description || 'No description';
3601
- const timestamp = new Date(task.created_at || task.timestamp || Date.now()).toLocaleString();
3602
- const status = task.status || 'completed';
1151
+ // Fallback: Enhanced basic markdown-like formatting
1152
+ formattedContent = this.preprocessTaskLists(formattedContent);
3603
1153
 
3604
- item.innerHTML = `
3605
- <div class="task-item-header">
3606
- <div class="task-session-id">${sessionId}</div>
3607
- <div class="task-timestamp">${timestamp}</div>
3608
- </div>
3609
- <div class="task-description">${this.truncateText(taskDesc, 100)}</div>
3610
- <div class="task-status">
3611
- <span class="status-dot ${status}"></span>
3612
- <span class="status-text">${status}</span>
3613
- </div>
3614
- `;
1154
+ // Bold text **text**
1155
+ formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
3615
1156
 
3616
- item.addEventListener('click', () => {
3617
- this.handleTaskItemClick(task);
3618
- });
1157
+ // Italic text *text*
1158
+ formattedContent = formattedContent.replace(/\*(.*?)\*/g, '<em>$1</em>');
3619
1159
 
3620
- return item;
3621
- }
3622
-
3623
- createSessionItem(session) {
3624
- const item = document.createElement('div');
3625
- item.className = 'session-item';
1160
+ // Code blocks ```code```
1161
+ formattedContent = formattedContent.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>');
3626
1162
 
3627
- const sessionId = session.session_id || 'Unknown';
3628
- const createdAt = new Date(session.created_at || session.timestamp || Date.now()).toLocaleString();
3629
- const lastActivity = session.last_activity ? new Date(session.last_activity).toLocaleString() : 'No activity';
3630
- const taskCount = session.task_count || 0;
3631
- const status = session.status || 'active';
1163
+ // Inline code `code`
1164
+ formattedContent = formattedContent.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
3632
1165
 
3633
- item.innerHTML = `
3634
- <div class="session-item-header">
3635
- <div class="session-id">${sessionId}</div>
3636
- <div class="session-timestamp">${createdAt}</div>
3637
- </div>
3638
- <div class="session-details">
3639
- <div class="session-info">
3640
- <span class="session-task-count">${taskCount} task(s)</span>
3641
- <span class="session-last-activity">Last: ${lastActivity}</span>
3642
- </div>
3643
- <div class="session-status">
3644
- <span class="status-dot ${status}"></span>
3645
- <span class="status-text">${status}</span>
3646
- </div>
3647
- </div>
3648
- `;
1166
+ // Headers
1167
+ formattedContent = formattedContent.replace(/^### (.*$)/gm, '<h3>$1</h3>');
1168
+ formattedContent = formattedContent.replace(/^## (.*$)/gm, '<h2>$1</h2>');
1169
+ formattedContent = formattedContent.replace(/^# (.*$)/gm, '<h1>$1</h1>');
3649
1170
 
3650
- // Add enhanced click handler with debugging
3651
- item.addEventListener('click', (event) => {
3652
- event.preventDefault();
3653
- event.stopPropagation();
3654
- this.handleSessionItemClick(session);
3655
- });
1171
+ // Lists - regular
1172
+ formattedContent = formattedContent.replace(/^- (.*$)/gm, '<li>$1</li>');
1173
+ formattedContent = formattedContent.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
3656
1174
 
3657
- // Add visual feedback for clickability
3658
- item.style.cursor = 'pointer';
3659
- item.setAttribute('title', `Click to load session: ${sessionId}`);
1175
+ // Convert URLs to links
1176
+ const httpUrlRegex = /(https?:\/\/[^\s]+)/g;
1177
+ formattedContent = formattedContent.replace(httpUrlRegex, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
3660
1178
 
3661
- return item;
1179
+ // Convert newlines to br tags
1180
+ formattedContent = formattedContent.replace(/\n/g, '<br>');
1181
+
1182
+ return formattedContent;
3662
1183
  }
3663
1184
 
3664
- // Click Handlers
3665
- async handleTaskItemClick(task) {
3666
- try {
3667
-
3668
- const sessionId = task.session_id;
3669
- if (!sessionId) {
3670
- console.error('[UIManager] No session ID found in task data:', task);
3671
- this.showNotification('Invalid task - no session ID found', 'error');
3672
- return;
3673
- }
3674
-
3675
-
3676
- // Close the modal first
3677
- this.closeModal();
3678
-
3679
- // Load the session and show logs
3680
- await this.loadSessionAndShowLogs(sessionId);
3681
-
3682
- } catch (error) {
3683
- console.error('[UIManager] Error in handleTaskItemClick:', error);
3684
- this.showNotification(`Failed to load task session: ${error.message}`, 'error');
1185
+ preprocessTaskLists(content) {
1186
+ // Convert markdown task lists to HTML checkboxes
1187
+ // - [ ] unchecked task -> checkbox unchecked
1188
+ // - [x] checked task -> checkbox checked
1189
+ // - [X] checked task -> checkbox checked
1190
+ return content
1191
+ .replace(/^- \[ \] (.*)$/gm, '<div class="task-item"><input type="checkbox" disabled> $1</div>')
1192
+ .replace(/^- \[[xX]\] (.*)$/gm, '<div class="task-item"><input type="checkbox" checked disabled> $1</div>');
1193
+ }
1194
+
1195
+ scrollActivityToBottom() {
1196
+ if (this.elements.activityLog) {
1197
+ this.elements.activityLog.scrollTop = this.elements.activityLog.scrollHeight;
3685
1198
  }
3686
1199
  }
3687
1200
 
3688
- async handleSessionItemClick(session) {
3689
- try {
3690
-
3691
- const sessionId = session.session_id;
3692
- if (!sessionId) {
3693
- console.error('[UIManager] No session ID found in session data:', session);
3694
- this.showNotification('Invalid session - no session ID found', 'error');
3695
- return;
3696
- }
3697
-
3698
-
3699
- // Close the modal first
3700
- this.closeModal();
3701
-
3702
- // Load the session and show logs
3703
- await this.loadSessionAndShowLogs(sessionId);
3704
-
3705
- } catch (error) {
3706
- console.error('[UIManager] Error in handleSessionItemClick:', error);
3707
- this.showNotification(`Failed to load session: ${error.message}`, 'error');
1201
+ clearTaskInput() {
1202
+ if (this.elements.taskInput) {
1203
+ this.elements.taskInput.value = '';
1204
+ }
1205
+
1206
+ if (this.elements.sendBtn) {
1207
+ this.elements.sendBtn.disabled = true;
3708
1208
  }
3709
1209
  }
3710
1210
 
3711
- async loadSessionAndShowLogs(sessionId) {
1211
+ async loadSession(sessionId) {
3712
1212
  // Check if task is running
3713
1213
  if (this.state.isTaskRunning) {
3714
1214
  const canProceed = await this.showTaskRunningWarning('load a different session');
@@ -3718,31 +1218,87 @@ class VibeSurfUIManager {
3718
1218
  try {
3719
1219
  this.showLoading('Loading session...');
3720
1220
 
3721
-
3722
- // Load the session in session manager
3723
1221
  const success = await this.sessionManager.loadSession(sessionId);
3724
1222
 
3725
- if (success) {
3726
- // Session manager will trigger the session loaded event
3727
- // which will update the UI automatically
3728
- this.showNotification('Session loaded successfully', 'success');
3729
- } else {
3730
- this.showNotification('Failed to load session - session may not exist', 'error');
3731
- }
3732
-
3733
1223
  this.hideLoading();
1224
+
1225
+ if (!success) {
1226
+ this.showNotification('Failed to load session', 'error');
1227
+ }
3734
1228
  } catch (error) {
3735
- console.error('[UIManager] Error in loadSessionAndShowLogs:', error);
3736
1229
  this.hideLoading();
3737
1230
  this.showNotification(`Failed to load session: ${error.message}`, 'error');
3738
1231
  }
3739
1232
  }
3740
1233
 
3741
- // Utility Methods
3742
- truncateText(text, maxLength) {
3743
- if (!text) return '';
3744
- if (text.length <= maxLength) return text;
3745
- return text.substring(0, maxLength) + '...';
1234
+ updateLLMProfileSelect() {
1235
+ if (!this.elements.llmProfileSelect) return;
1236
+
1237
+ const profiles = this.settingsManager.getLLMProfiles();
1238
+ const select = this.elements.llmProfileSelect;
1239
+ select.innerHTML = '';
1240
+
1241
+ if (profiles.length === 0) {
1242
+ // Add placeholder option when no profiles available
1243
+ const placeholderOption = document.createElement('option');
1244
+ placeholderOption.value = '';
1245
+ placeholderOption.textContent = 'No LLM profiles configured - Go to Settings';
1246
+ placeholderOption.disabled = true;
1247
+ placeholderOption.selected = true;
1248
+ select.appendChild(placeholderOption);
1249
+ } else {
1250
+ // Add default empty option
1251
+ const emptyOption = document.createElement('option');
1252
+ emptyOption.value = '';
1253
+ emptyOption.textContent = 'Select LLM Profile...';
1254
+ emptyOption.disabled = true;
1255
+ select.appendChild(emptyOption);
1256
+
1257
+ // Add actual profiles
1258
+ profiles.forEach(profile => {
1259
+ const option = document.createElement('option');
1260
+ option.value = profile.profile_name;
1261
+ option.textContent = profile.profile_name;
1262
+ if (profile.is_default) {
1263
+ option.selected = true;
1264
+ }
1265
+ select.appendChild(option);
1266
+ });
1267
+ }
1268
+
1269
+ // Update send button state if taskInput exists
1270
+ if (this.elements.taskInput) {
1271
+ this.handleTaskInputChange({ target: this.elements.taskInput });
1272
+ }
1273
+ }
1274
+
1275
+ showLLMProfileRequiredModal(action) {
1276
+ const isConfigureAction = action === 'configure';
1277
+ const title = isConfigureAction ? 'LLM Profile Required' : 'Please Select LLM Profile';
1278
+ const message = isConfigureAction
1279
+ ? 'No LLM profiles are configured. You need to configure at least one LLM profile before sending tasks.'
1280
+ : 'Please select an LLM profile from the dropdown to proceed with your task.';
1281
+
1282
+ const options = isConfigureAction
1283
+ ? {
1284
+ confirmText: 'Open Settings',
1285
+ cancelText: 'Cancel',
1286
+ onConfirm: () => {
1287
+ this.handleShowSettings();
1288
+ }
1289
+ }
1290
+ : {
1291
+ confirmText: 'OK',
1292
+ cancelText: 'Open Settings',
1293
+ onConfirm: () => {
1294
+ this.elements.llmProfileSelect?.focus();
1295
+ },
1296
+ onCancel: () => {
1297
+ this.handleShowSettings();
1298
+ }
1299
+ };
1300
+
1301
+ this.modalManager.showWarningModal(title, message, options);
3746
1302
  }
3747
1303
 
3748
1304
  // Loading and notifications
@@ -3790,7 +1346,6 @@ class VibeSurfUIManager {
3790
1346
  type: chromeType
3791
1347
  }
3792
1348
  });
3793
-
3794
1349
  }
3795
1350
 
3796
1351
  // Initialization
@@ -3798,8 +1353,8 @@ class VibeSurfUIManager {
3798
1353
  try {
3799
1354
  this.showLoading('Initializing VibeSurf...');
3800
1355
 
3801
- // Load settings data
3802
- await this.loadSettingsData();
1356
+ // Load settings data through settings manager
1357
+ await this.settingsManager.loadSettingsData();
3803
1358
 
3804
1359
  // Create initial session if none exists
3805
1360
  if (!this.sessionManager.getCurrentSession()) {
@@ -3819,7 +1374,36 @@ class VibeSurfUIManager {
3819
1374
  // Stop task status monitoring
3820
1375
  this.stopTaskStatusMonitoring();
3821
1376
 
3822
- // Remove event listeners if needed
1377
+ // Cleanup managers
1378
+ if (this.settingsManager) {
1379
+ // Cleanup settings manager if it has destroy method
1380
+ if (typeof this.settingsManager.destroy === 'function') {
1381
+ this.settingsManager.destroy();
1382
+ }
1383
+ }
1384
+
1385
+ if (this.historyManager) {
1386
+ // Cleanup history manager if it has destroy method
1387
+ if (typeof this.historyManager.destroy === 'function') {
1388
+ this.historyManager.destroy();
1389
+ }
1390
+ }
1391
+
1392
+ if (this.fileManager) {
1393
+ // Cleanup file manager if it has destroy method
1394
+ if (typeof this.fileManager.destroy === 'function') {
1395
+ this.fileManager.destroy();
1396
+ }
1397
+ }
1398
+
1399
+ if (this.modalManager) {
1400
+ // Cleanup modal manager if it has destroy method
1401
+ if (typeof this.modalManager.destroy === 'function') {
1402
+ this.modalManager.destroy();
1403
+ }
1404
+ }
1405
+
1406
+ // Clear state
3823
1407
  this.state.currentModal = null;
3824
1408
  }
3825
1409
  }