agentgui 1.0.65 → 1.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/app.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const BASE_URL = window.__BASE_URL || '';
2
2
 
3
- // Auto-reconnecting WebSocket wrapper
4
3
  class ReconnectingWebSocket {
5
4
  constructor(url, options = {}) {
6
5
  this.url = url;
@@ -79,1707 +78,343 @@ class ReconnectingWebSocket {
79
78
 
80
79
  class GMGUIApp {
81
80
  constructor() {
82
- this.agents = new Map();
83
- this.selectedAgent = null;
84
81
  this.conversations = new Map();
85
82
  this.currentConversation = null;
86
- this.activeStream = null;
87
- this.pollingInterval = null;
88
- this.syncWs = null;
89
- this.broadcastChannel = null;
90
- this.settings = { autoScroll: true, connectTimeout: 30000 };
91
- this.pendingMessages = new Map();
92
- this.idempotencyKeys = new Map();
93
-
94
- // Start async initialization and handle errors
95
- this.initPromise = this.init().catch(err => {
96
- console.error('[CRITICAL] GMGUIApp.init() failed:', err);
97
- console.error('[CRITICAL] Stack:', err.stack);
98
- throw err;
99
- });
100
- }
101
-
102
- // Helper for authenticated API calls - ensures credentials sent for proxy auth
103
- async apiFetch(url, options = {}) {
104
- return fetch(url, { credentials: 'include', ...options });
105
- }
106
-
107
- async init() {
108
- console.log('[DEBUG] Init: Starting initialization');
109
- console.log('[DEBUG] Init: BASE_URL =', BASE_URL);
110
- console.log('[DEBUG] Init: Window width:', window.innerWidth);
83
+ this.agents = new Map();
84
+ this.selectedAgent = null;
85
+ this.ws = null;
86
+ }
111
87
 
112
- // Ensure sidebar is visible on desktop (open on wide screens)
113
- const sidebar = document.getElementById('sidebar');
114
- if (window.innerWidth >= 768 && sidebar) {
115
- console.log('[DEBUG] Init: Wide screen detected, ensuring sidebar is visible');
116
- sidebar.classList.remove('open'); // On desktop, sidebar is always visible, no need for 'open' class
117
- } else if (sidebar) {
118
- console.log('[DEBUG] Init: Mobile/narrow screen detected, opening sidebar');
119
- sidebar.classList.add('open');
120
- }
88
+ async init() {
89
+ console.log('[APP] Initializing');
121
90
 
122
- this.loadSettings();
123
91
  this.setupEventListeners();
124
- await this.fetchHome();
125
- console.log('[DEBUG] Init: Fetched home');
126
92
  await this.fetchAgents();
127
- console.log('[DEBUG] Init: Fetched agents, count:', this.agents.size);
128
93
 
129
- // Pre-select agent on first load: try from localStorage, otherwise pick first available
130
94
  const savedAgent = localStorage.getItem('gmgui-selectedAgent');
131
95
  if (savedAgent && this.agents.has(savedAgent)) {
132
96
  this.selectedAgent = savedAgent;
133
- console.log('[DEBUG] Init: Restored selected agent from localStorage:', savedAgent);
134
97
  } else if (this.agents.size > 0) {
135
98
  this.selectedAgent = Array.from(this.agents.keys())[0];
136
99
  localStorage.setItem('gmgui-selectedAgent', this.selectedAgent);
137
- console.log('[DEBUG] Init: Pre-selected first available agent:', this.selectedAgent);
138
100
  }
139
101
 
140
- await this.autoImportClaudeCode();
141
- console.log('[DEBUG] Init: Auto-imported Claude Code conversations');
142
102
  await this.fetchConversations();
143
- console.log('[DEBUG] Init: Fetched conversations, count:', this.conversations.size);
144
- console.log('[DEBUG] Init: Conversation details:', Array.from(this.conversations.values()).slice(0, 3));
145
- this.connectSyncWebSocket();
146
- this.setupCrossTabSync();
147
- this.startPeriodicSync();
148
- console.log('[DEBUG] Init: About to renderAll with', this.conversations.size, 'conversations');
103
+ this.connectWebSocket();
149
104
  this.renderAll();
150
- console.log('[DEBUG] Init: renderAll completed');
151
- console.log('[DEBUG] Init: chatList innerHTML length:', document.getElementById('chatList')?.innerHTML?.length || 0);
152
- }
153
-
154
- startPeriodicSync() {
155
- // GUARANTEED CONSISTENCY MECHANISM
156
- // Primary: WebSocket events (real-time, instant)
157
- // Fallback: Consistency check every 3 seconds
158
- // If any mismatch detected, full refresh immediately
159
-
160
- // Server auto-import runs every 30 seconds (discovers new Claude Code conversations)
161
- setInterval(() => {
162
- this.autoImportClaudeCode();
163
- }, 30000);
164
-
165
- // Consistency monitor: Verify local state matches server
166
- // This catches any desync issues and fixes them within 3 seconds
167
- setInterval(() => {
168
- this.verifyConsistency();
169
- }, 3000);
170
- }
171
-
172
- async verifyConsistency() {
173
- // Silent consistency check - only log if mismatch found
174
- try {
175
- const res = await this.apiFetch(BASE_URL + '/api/conversations');
176
- if (!res.ok) return;
177
-
178
- const data = await res.json();
179
- const serverCount = data.conversations?.length || 0;
180
- const localCount = this.conversations.size;
181
-
182
- if (serverCount !== localCount) {
183
- console.warn(`[CONSISTENCY MISMATCH] Server has ${serverCount} conversations, local has ${localCount}`);
184
- console.warn('[CONSISTENCY] Forcing full refresh to restore sync');
185
- await this.fetchConversations();
186
- this.renderChatHistory();
187
- console.log('[CONSISTENCY] State restored to match server');
188
- }
189
- } catch (e) {
190
- // Silent error - don't spam logs
191
- }
105
+ console.log('[APP] Ready');
192
106
  }
193
107
 
194
- async autoImportClaudeCode() {
195
- try {
196
- await this.apiFetch(BASE_URL + '/api/import/claude-code');
197
- } catch (e) {
198
- console.error('autoImportClaudeCode:', e);
199
- }
200
- }
201
-
202
- connectSyncWebSocket() {
203
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
204
- this.syncWs = new ReconnectingWebSocket(
205
- `${proto}//${location.host}${BASE_URL}/sync`
206
- );
207
-
208
- this.wsDisconnectTime = null;
209
-
210
- this.syncWs.on('open', () => {
211
- console.log('[SYNC] WebSocket connected - guaranteed consistency active');
212
- this.updateConnectionStatus('connected');
213
- this.wsDisconnectTime = null;
214
-
215
- // Force full sync when reconnecting to ensure consistency
216
- this.fetchConversations().then(() => this.renderChatHistory());
217
- });
218
-
219
- this.syncWs.on('message', (e) => {
220
- try {
221
- const event = JSON.parse(e.data);
222
- console.log('[SYNC] Event:', event.type);
223
- this.handleSyncEvent(event, false);
224
- } catch (err) {
225
- console.error('[SYNC ERROR] Parse error:', err);
226
- }
227
- });
228
-
229
- this.syncWs.on('close', () => {
230
- console.log('[SYNC] WebSocket disconnected - reconnecting...');
231
- this.updateConnectionStatus('reconnecting');
232
- this.wsDisconnectTime = Date.now();
233
-
234
- // CRITICAL: Force full refresh if disconnected for more than 2 seconds
235
- // This ensures we NEVER have inconsistent state for more than a few seconds
236
- setTimeout(() => {
237
- if (this.wsDisconnectTime && Date.now() - this.wsDisconnectTime > 2000) {
238
- console.log('[SYNC CRITICAL] Lost WebSocket > 2s, forcing full data refresh NOW');
239
- this.fetchConversations().then(() => {
240
- this.renderChatHistory();
241
- console.log('[SYNC] Full refresh completed - guaranteed consistency restored');
242
- });
243
- }
244
- }, 2000);
245
- });
246
-
247
- this.syncWs.on('error', (err) => {
248
- console.error('[SYNC ERROR]', err);
249
- this.updateConnectionStatus('disconnected');
250
- });
251
- }
252
-
253
- setupCrossTabSync() {
254
- if ('BroadcastChannel' in window) {
255
- try {
256
- this.broadcastChannel = new BroadcastChannel('gmgui-sync');
257
- this.broadcastChannel.onmessage = (e) => {
258
- this.handleSyncEvent(e.data, true);
259
- };
260
- } catch (err) {
261
- console.error('BroadcastChannel error:', err);
262
- }
263
- }
264
- }
265
-
266
- handleSyncEvent(event, fromBroadcast = false) {
267
- // CRITICAL: Server is the authoritative source of truth
268
- // Real-time WebSocket events for messages arrive immediately
269
- // Subscribe to conversation to receive message updates
270
-
271
- console.log('[STATE SYNC] Event received:', event.type);
272
-
273
- switch (event.type) {
274
- case 'sync_connected':
275
- console.log('[STATE SYNC] Connected to sync bus - subscribing to all active sessions');
276
- // On connection, always do a full state refresh
277
- this.fetchConversations().then(() => this.renderChatHistory());
278
- break;
279
-
280
- case 'conversation_created':
281
- console.log('[STATE SYNC] Conversation created, fetching full state');
282
- // Never trust just the event data - fetch authoritative state
283
- this.fetchConversations().then(() => this.renderChatHistory());
284
- if (!fromBroadcast && this.broadcastChannel) {
285
- this.broadcastChannel.postMessage(event);
286
- }
287
- break;
288
-
289
- case 'conversation_updated':
290
- console.log('[STATE SYNC] Conversation updated, fetching full state');
291
- // Fetch full state to ensure we have the latest version
292
- this.fetchConversations().then(() => {
293
- this.renderChatHistory();
294
- // If we're viewing this conversation, refresh its content too
295
- if (this.currentConversation === event.conversation?.id) {
296
- this.displayConversation(event.conversation.id);
297
- }
298
- });
299
- if (!fromBroadcast && this.broadcastChannel) {
300
- this.broadcastChannel.postMessage(event);
301
- }
302
- break;
303
-
304
- case 'conversation_deleted':
305
- console.log('[STATE SYNC] Conversation deleted, fetching full state');
306
- this.fetchConversations().then(() => {
307
- this.renderChatHistory();
308
- if (this.currentConversation === event.conversationId) {
309
- this.currentConversation = null;
310
- this.renderCurrentConversation();
311
- }
312
- });
313
- if (!fromBroadcast && this.broadcastChannel) {
314
- this.broadcastChannel.postMessage(event);
315
- }
316
- break;
317
-
318
- case 'conversations_updated':
319
- console.log('[STATE SYNC] Conversations imported, fetching full state');
320
- // New conversations imported - refresh everything
321
- this.fetchConversations().then(() => this.renderChatHistory());
322
- if (!fromBroadcast && this.broadcastChannel) {
323
- this.broadcastChannel.postMessage(event);
324
- }
325
- break;
326
-
327
- case 'message_created':
328
- console.log('[STATE SYNC] Message created via WebSocket - real-time push');
329
- // User message was created - add it immediately without polling
330
- if (this.currentConversation === event.conversationId && event.message) {
331
- console.log('[STATE SYNC] Adding user message to display immediately');
332
- // Stop any existing polling for this conversation
333
- this.stopPollingMessages();
334
- // Add message directly to display
335
- this.addMessageToDisplay(event.message);
336
- // Update conversation metadata
337
- this.fetchConversations().then(() => this.renderChatHistory());
338
- // Auto-scroll to new message
339
- if (this.settings.autoScroll) {
340
- setTimeout(() => {
341
- const div = document.getElementById('chatMessages');
342
- if (div) div.scrollTop = div.scrollHeight;
343
- }, 50);
344
- }
345
- } else {
346
- // Not viewing this conversation, just update timestamps
347
- this.fetchConversations().then(() => this.renderChatHistory());
348
- }
349
- if (!fromBroadcast && this.broadcastChannel) {
350
- this.broadcastChannel.postMessage(event);
351
- }
352
- break;
353
-
354
- case 'session_updated':
355
- console.log('[STATE SYNC] Session updated via WebSocket:', event.status, '- real-time push');
356
- // Session completed - agent response arrived via WebSocket push (no polling!)
357
- if (this.currentConversation === event.conversationId && event.message) {
358
- console.log('[STATE SYNC] Adding assistant message to display immediately (real-time push)');
359
- // Stop polling immediately
360
- this.stopPollingMessages();
361
- // Add message directly to display
362
- this.addMessageToDisplay(event.message);
363
- // Update conversation metadata
364
- this.fetchConversations().then(() => this.renderChatHistory());
365
- // Auto-scroll to new message
366
- if (this.settings.autoScroll) {
367
- setTimeout(() => {
368
- const div = document.getElementById('chatMessages');
369
- if (div) div.scrollTop = div.scrollHeight;
370
- }, 50);
371
- }
372
- } else {
373
- // Not viewing this conversation, just update timestamps
374
- this.fetchConversations().then(() => this.renderChatHistory());
375
- }
376
- if (!fromBroadcast && this.broadcastChannel) {
377
- this.broadcastChannel.postMessage(event);
378
- }
379
- break;
380
- }
381
- }
382
-
383
- updateConnectionStatus(status) {
384
- const el = document.getElementById('connectionStatus');
385
- if (!el) return;
386
-
387
- el.className = `connection-status ${status}`;
388
- const text = el.querySelector('.status-text');
389
- if (text) {
390
- text.textContent = status === 'connected' ? 'Connected' :
391
- status === 'reconnecting' ? 'Reconnecting...' :
392
- 'Disconnected';
393
- }
394
- }
395
-
396
- async fetchHome() {
397
- try {
398
- const res = await this.apiFetch(BASE_URL + '/api/home');
399
- if (res.ok) {
400
- const data = await res.json();
401
- localStorage.setItem('gmgui-home', data.home);
402
- }
403
- } catch (e) {
404
- console.error('fetchHome:', e);
405
- }
406
- }
407
-
408
- loadSettings() {
409
- const stored = localStorage.getItem('gmgui-settings');
410
- if (stored) {
411
- try { this.settings = { ...this.settings, ...JSON.parse(stored) }; } catch (_) {}
412
- }
413
- this.applySettings();
414
- }
415
-
416
- saveSettings() {
417
- localStorage.setItem('gmgui-settings', JSON.stringify(this.settings));
418
- }
419
-
420
- applySettings() {
421
- const el = document.getElementById('autoScroll');
422
- if (el) el.checked = this.settings.autoScroll;
423
- const t = document.getElementById('connectTimeout');
424
- if (t) t.value = this.settings.connectTimeout / 1000;
425
- }
426
-
427
- expandHome(p) {
428
- if (!p) return p;
429
- const home = localStorage.getItem('gmgui-home') || '/config';
430
- return p.startsWith('~') ? p.replace('~', home) : p;
431
- }
432
-
433
- setupEventListeners() {
434
- window.addEventListener('focus', () => {
435
- this.autoImportClaudeCode().then(() => {
436
- this.fetchConversations().then(() => this.renderChatHistory());
437
- });
438
- });
439
-
440
- // THEME CHANGE LISTENER: Update HTML blocks when theme changes
441
- // Listen for theme changes on document element
442
- const themeObserver = new MutationObserver(() => {
443
- console.log('[THEME] Theme changed, updating HTML blocks');
444
- this.updateHtmlBlockThemes();
445
- });
446
-
447
- themeObserver.observe(document.documentElement, {
448
- attributes: true,
449
- attributeFilter: ['data-theme']
450
- });
451
-
452
- // Also listen for system theme changes
453
- if (window.matchMedia) {
454
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
455
- console.log('[THEME] System theme changed, updating HTML blocks');
456
- this.updateHtmlBlockThemes();
457
- });
458
- }
459
-
460
- const input = document.getElementById('messageInput');
461
- if (input) {
462
- input.addEventListener('keydown', (e) => {
463
- if (e.key === 'Enter' && !e.shiftKey) {
464
- e.preventDefault();
465
- this.sendMessage();
466
- }
467
- });
468
- input.addEventListener('input', () => this.updateSendButtonState());
469
- }
470
- document.getElementById('autoScroll')?.addEventListener('change', (e) => {
471
- this.settings.autoScroll = e.target.checked;
472
- this.saveSettings();
473
- });
474
- document.getElementById('connectTimeout')?.addEventListener('change', (e) => {
475
- this.settings.connectTimeout = parseInt(e.target.value) * 1000;
476
- this.saveSettings();
477
- });
478
- }
479
-
480
- updateHtmlBlockThemes() {
481
- // Update theme attribute and CSS for all existing HTML blocks
482
- const currentTheme = document.documentElement.getAttribute('data-theme') ||
483
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
484
-
485
- // CRITICAL: Remove old theme styles and inject new ones for all HTML blocks
486
- document.querySelectorAll('.html-content').forEach(content => {
487
- // Remove any existing theme style tags
488
- const oldStyles = content.querySelectorAll('style');
489
- oldStyles.forEach(style => {
490
- if (style.textContent.includes('.html-content')) {
491
- style.remove();
492
- }
493
- });
494
-
495
- // Inject new theme-aware CSS
496
- const themeCSS = currentTheme === 'dark'
497
- ? `<style>
498
- .html-content {
499
- color: #f8fafc;
500
- background: transparent;
501
- }
502
- .html-content p { color: #cbd5e1; }
503
- .html-content h1, .html-content h2, .html-content h3,
504
- .html-content h4, .html-content h5, .html-content h6 {
505
- color: #f8fafc;
506
- }
507
- .html-content a { color: #6366f1; }
508
- .html-content code { color: #c7d2fe; background: rgba(0,0,0,0.3); }
509
- .html-content pre { background: rgba(0,0,0,0.5); color: #e0e7ff; }
510
- .html-content table { border-color: #334155; }
511
- .html-content th { background: #1a202c; color: #f8fafc; }
512
- .html-content td { border-color: #334155; }
513
- .html-content blockquote { border-color: #334155; color: #cbd5e1; }
514
- .html-content ul, .html-content ol { color: #cbd5e1; }
515
- .html-content li { color: #cbd5e1; }
516
- </style>`
517
- : `<style>
518
- .html-content {
519
- color: #1d2129;
520
- background: transparent;
521
- }
522
- .html-content p { color: #475569; }
523
- .html-content h1, .html-content h2, .html-content h3,
524
- .html-content h4, .html-content h5, .html-content h6 {
525
- color: #1d2129;
526
- }
527
- .html-content a { color: #4f46e5; }
528
- .html-content code { color: #6366f1; background: rgba(99,102,241,0.1); }
529
- .html-content pre { background: #f3f4f6; color: #1d2129; }
530
- .html-content table { border-color: #e5e7eb; }
531
- .html-content th { background: #f9fafb; color: #1d2129; }
532
- .html-content td { border-color: #e5e7eb; }
533
- .html-content blockquote { border-color: #e5e7eb; color: #475569; }
534
- .html-content ul, .html-content ol { color: #475569; }
535
- .html-content li { color: #475569; }
536
- </style>`;
537
-
538
- // Create a temporary wrapper to parse and insert the style
539
- const tempDiv = document.createElement('div');
540
- tempDiv.innerHTML = themeCSS;
541
- const styleEl = tempDiv.querySelector('style');
542
- if (styleEl) {
543
- content.insertBefore(styleEl.cloneNode(true), content.firstChild);
544
- }
545
-
546
- // Update data-theme attribute
547
- content.setAttribute('data-theme', currentTheme);
548
- });
549
-
550
- console.log(`[THEME] Updated ${document.querySelectorAll('.html-content').length} HTML blocks to ${currentTheme} mode`);
551
- }
552
-
553
108
  async fetchAgents() {
554
109
  try {
555
- const res = await this.apiFetch(BASE_URL + '/api/agents');
110
+ const res = await fetch(BASE_URL + '/api/agents');
556
111
  const data = await res.json();
557
- if (data.agents) {
558
- data.agents.forEach(a => this.agents.set(a.id, a));
112
+ for (const agent of data.agents || []) {
113
+ this.agents.set(agent.id, agent);
559
114
  }
560
115
  } catch (e) {
561
- console.error('fetchAgents:', e);
116
+ console.error('[APP] Error fetching agents:', e);
562
117
  }
563
118
  }
564
119
 
565
120
  async fetchConversations() {
566
121
  try {
567
- console.log('[DEBUG] fetchConversations: Starting fetch from', BASE_URL + '/api/conversations');
568
- const res = await this.apiFetch(BASE_URL + '/api/conversations');
569
- console.log('[DEBUG] fetchConversations: Response status:', res.status);
570
-
571
- if (!res.ok) {
572
- console.error('[DEBUG] fetchConversations: Response not OK, status:', res.status);
573
- return;
574
- }
575
-
122
+ const res = await fetch(BASE_URL + '/api/conversations');
576
123
  const data = await res.json();
577
- console.log('[DEBUG] fetchConversations response count:', data.conversations?.length);
578
-
579
- if (data.conversations) {
580
- console.log('[DEBUG] fetchConversations: About to clear and load conversations');
581
- this.conversations.clear();
582
- console.log('[DEBUG] fetchConversations: Cleared conversations map, size now:', this.conversations.size);
583
-
584
- data.conversations.forEach(c => {
585
- this.conversations.set(c.id, c);
586
- });
587
-
588
- console.log('[DEBUG] Loaded conversations, total:', this.conversations.size);
589
- console.log('[DEBUG] First few conversation IDs:', Array.from(this.conversations.keys()).slice(0, 5));
590
-
591
- if (this.conversations.size === 0) {
592
- console.error('[DEBUG] ERROR: conversations.size is 0 after loading!');
593
- }
594
- } else {
595
- console.warn('[DEBUG] fetchConversations: data.conversations is undefined or null');
596
- console.warn('[DEBUG] fetchConversations: Full response:', data);
124
+ this.conversations.clear();
125
+ for (const conv of data.conversations || []) {
126
+ this.conversations.set(conv.id, conv);
597
127
  }
128
+ console.log('[APP] Loaded', this.conversations.size, 'conversations');
598
129
  } catch (e) {
599
- console.error('[DEBUG] fetchConversations error:', e);
600
- console.error('[DEBUG] Error details:', e.message, e.stack);
130
+ console.error('[APP] Error fetching conversations:', e);
601
131
  }
602
132
  }
603
133
 
604
134
  async fetchMessages(conversationId) {
605
135
  try {
606
- const res = await this.apiFetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
136
+ const res = await fetch(BASE_URL + `/api/conversations/${conversationId}/messages`);
607
137
  const data = await res.json();
608
138
  return data.messages || [];
609
139
  } catch (e) {
610
- console.error('fetchMessages:', e);
140
+ console.error('[APP] Error fetching messages:', e);
611
141
  return [];
612
142
  }
613
143
  }
614
144
 
615
- renderAll() {
616
- console.log('[DEBUG] renderAll: Called with', this.conversations.size, 'conversations');
617
- this.renderAgentCards();
618
- this.renderChatHistory();
619
- if (this.currentConversation) {
620
- console.log('[DEBUG] renderAll: Displaying current conversation', this.currentConversation);
621
- this.displayConversation(this.currentConversation);
622
- }
623
- }
624
-
625
- renderAgentCards() {
626
- const container = document.getElementById('agentCards');
627
- if (!container) return;
628
- container.innerHTML = '';
629
- if (this.agents.size === 0) {
630
- container.innerHTML = '<p style="color: var(--text-tertiary); font-size: 0.875rem;">No agents found. Install claude or opencode.</p>';
631
- return;
632
- }
633
- let first = true;
634
- this.agents.forEach((agent, id) => {
635
- if (!first) {
636
- const sep = document.createElement('span');
637
- sep.className = 'agent-separator';
638
- sep.textContent = '|';
639
- container.appendChild(sep);
640
- }
641
- first = false;
642
- const card = document.createElement('button');
643
- card.className = `agent-card ${this.selectedAgent === id ? 'active' : ''}`;
644
- card.onclick = () => this.selectAgent(id);
645
- card.innerHTML = `
646
- <span class="agent-card-icon">${escapeHtml(agent.icon || 'A')}</span>
647
- <span class="agent-card-name">${escapeHtml(agent.name || id)}</span>
648
- `;
649
- container.appendChild(card);
650
- });
651
- }
652
-
653
- selectAgent(id) {
654
- this.selectedAgent = id;
655
- localStorage.setItem('gmgui-selectedAgent', id);
656
- this.renderAgentCards();
657
- const welcome = document.querySelector('.welcome-section');
658
- if (welcome) welcome.style.display = 'none';
659
- const input = document.getElementById('messageInput');
660
- if (input) input.focus();
661
- }
145
+ connectWebSocket() {
146
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
147
+ this.ws = new ReconnectingWebSocket(`${proto}//${location.host}${BASE_URL}/sync`);
662
148
 
663
- renderChatHistory() {
664
- const list = document.getElementById('chatList');
665
- if (!list) {
666
- console.error('[DEBUG] chatList element not found!');
667
- return;
668
- }
669
- list.innerHTML = '';
670
- console.log('[DEBUG] renderChatHistory - conversations.size:', this.conversations.size);
671
-
672
- // Debug: Update page title with conversation count
673
- document.title = `GMGUI (${this.conversations.size} chats)`;
674
-
675
- if (this.conversations.size === 0) {
676
- console.warn('[DEBUG] No conversations to display - showing empty state');
677
- console.warn('[DEBUG] conversations map contents:', this.conversations);
678
-
679
- // VISUAL DEBUG: Show debug info on page
680
- const debugInfo = `
681
- <div style="background: #fee; padding: 1rem; border: 1px solid #f99; border-radius: 0.5rem; margin-bottom: 1rem; font-family: monospace; font-size: 0.75rem;">
682
- <strong style="color: #c00;">🔍 DEBUG INFO</strong><br>
683
- Conversations: ${this.conversations.size}<br>
684
- BASE_URL: ${BASE_URL}<br>
685
- Width: ${window.innerWidth}px<br>
686
- Sidebar: ${document.getElementById('sidebar')?.offsetHeight > 0 ? 'visible' : 'hidden'}<br>
687
- <br>
688
- <strong>To debug (F12 console):</strong><br>
689
- • app.conversations.size<br>
690
- • Array.from(app.conversations.keys()).slice(0,5)
691
- </div>
692
- `;
693
-
694
- list.innerHTML = debugInfo + '<p style="color: var(--text-tertiary); font-size: 0.875rem; padding: 0.5rem;">No chats yet</p>';
695
- return;
696
- }
697
- const sorted = Array.from(this.conversations.values()).sort(
698
- (a, b) => (b.updated_at || 0) - (a.updated_at || 0)
699
- );
700
- console.log('[DEBUG] renderChatHistory - sorted conversations count:', sorted.length);
701
- console.log('[DEBUG] renderChatHistory - rendering', sorted.length, 'conversations');
702
- sorted.forEach(conv => {
703
- const item = document.createElement('button');
704
- item.className = `chat-item ${this.currentConversation === conv.id ? 'active' : ''}`;
705
- const titleSpan = document.createElement('span');
706
- titleSpan.className = 'chat-item-title';
707
- titleSpan.textContent = conv.title || 'Untitled';
708
- const deleteBtn = document.createElement('button');
709
- deleteBtn.className = 'chat-item-delete';
710
- deleteBtn.textContent = 'x';
711
- deleteBtn.title = 'Delete chat';
712
- deleteBtn.onclick = (e) => {
713
- e.stopPropagation();
714
- this.deleteConversation(conv.id);
715
- };
716
- item.appendChild(titleSpan);
717
- item.appendChild(deleteBtn);
718
- item.onclick = () => this.displayConversation(conv.id);
719
- list.appendChild(item);
149
+ this.ws.on('open', () => {
150
+ console.log('[WS] Connected');
151
+ document.getElementById('connectionStatus').textContent = 'Connected';
720
152
  });
721
- }
722
-
723
- async deleteConversation(id) {
724
- try {
725
- const res = await this.apiFetch(`${BASE_URL}/api/conversations/${id}`, { method: 'DELETE' });
726
- if (!res.ok) {
727
- console.error('deleteConversation failed:', res.status);
728
- return;
729
- }
730
- this.conversations.delete(id);
731
- if (this.currentConversation === id) {
732
- this.currentConversation = null;
733
- const first = Array.from(this.conversations.values())[0];
734
- if (first) {
735
- this.displayConversation(first.id);
736
- } else {
737
- this.showWelcome();
738
- }
739
- }
740
- this.renderChatHistory();
741
- } catch (e) {
742
- console.error('deleteConversation:', e);
743
- }
744
- }
745
-
746
- showWelcome() {
747
- const div = document.getElementById('chatMessages');
748
- if (!div) return;
749
- div.innerHTML = `
750
- <div class="welcome-section">
751
- <h2>Hi, what's your plan for today?</h2>
752
- <div class="agent-selection">
753
- <div id="agentCards" class="agent-cards"></div>
754
- </div>
755
- </div>
756
- `;
757
- this.renderAgentCards();
758
- }
759
153
 
760
- groupConsecutiveMessages(messages) {
761
- if (!messages.length) return [];
762
- const grouped = [];
763
- let current = { ...messages[0], content: typeof messages[0].content === 'string' ? messages[0].content : messages[0].content };
764
- for (let i = 1; i < messages.length; i++) {
765
- const msg = messages[i];
766
- if (msg.role === current.role && msg.role === 'assistant') {
767
- const curText = typeof current.content === 'string' ? current.content : (current.content?.text || '');
768
- const msgText = typeof msg.content === 'string' ? msg.content : (msg.content?.text || '');
769
- current = { ...current, content: curText + '\n\n' + msgText };
770
- } else {
771
- grouped.push(current);
772
- current = { ...msg };
773
- }
774
- }
775
- grouped.push(current);
776
- return grouped;
777
- }
778
-
779
- async displayConversation(id) {
780
- // CONSISTENCY CHECK: Verify conversation exists before displaying
781
- this.currentConversation = id;
782
- const conv = this.conversations.get(id);
783
- if (!conv) {
784
- console.warn('[SYNC] Conversation not found locally, fetching fresh data...');
785
- await this.fetchConversations();
786
- const freshConv = this.conversations.get(id);
787
- if (!freshConv) {
788
- console.error('[SYNC] Conversation still not found after refresh!');
789
- return;
790
- }
791
- }
792
- if (conv.agentId && !this.selectedAgent) {
793
- this.selectedAgent = conv.agentId;
794
- }
795
-
796
- const messages = await this.fetchMessages(id);
797
-
798
- const div = document.getElementById('chatMessages');
799
- if (!div) return;
800
- div.innerHTML = '';
801
-
802
- if (messages.length === 0 && !this.selectedAgent) {
803
- div.innerHTML = `
804
- <div class="welcome-section">
805
- <h2>Hi, what's your plan for today?</h2>
806
- <div class="agent-selection">
807
- <div id="agentCards" class="agent-cards"></div>
808
- </div>
809
- </div>
810
- `;
811
- this.renderAgentCards();
812
- } else {
813
- const grouped = this.groupConsecutiveMessages(messages);
814
- grouped.forEach(msg => this.addMessageToDisplay(msg));
815
-
816
- if (this.settings.autoScroll) {
817
- div.scrollTop = div.scrollHeight;
154
+ this.ws.on('message', (e) => {
155
+ try {
156
+ const event = JSON.parse(e.data);
157
+ this.handleEvent(event);
158
+ } catch (err) {
159
+ console.error('[WS] Parse error:', err);
818
160
  }
819
- }
820
- this.renderChatHistory();
821
- this.renderAgentCards();
822
- }
823
-
161
+ });
824
162
 
825
- sanitizeHtml(raw) {
826
- const tmp = document.createElement('div');
827
- tmp.innerHTML = raw;
828
- tmp.querySelectorAll('script,iframe,object,embed,form,meta,link').forEach(el => el.remove());
829
- tmp.querySelectorAll('*').forEach(el => {
830
- for (const attr of Array.from(el.attributes)) {
831
- if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
832
- if (attr.name === 'href' && attr.value.trim().toLowerCase().startsWith('javascript:')) el.removeAttribute(attr.name);
833
- }
163
+ this.ws.on('close', () => {
164
+ console.log('[WS] Disconnected, reconnecting...');
165
+ document.getElementById('connectionStatus').textContent = 'Reconnecting...';
834
166
  });
835
- return tmp.innerHTML;
836
- }
837
167
 
838
- looksLikeHtml(text) {
839
- const trimmed = text.trim();
840
- // Check for HTML tags at the start
841
- if (/^<[a-z][\s\S]*>/i.test(trimmed)) return true;
842
- // Check for closing tags of common HTML elements
843
- if (/<\/(div|span|p|table|ul|ol|h[1-6]|section|article|header|footer|nav|main|aside|details|summary|figure|figcaption|blockquote|pre|code|a|strong|em|img|br|hr|button|input|form|label)>/i.test(trimmed)) return true;
844
- // Check for Tailwind/RippleUI classes (strong indicator of HTML)
845
- if (/class\s*=\s*["'][^"']*(?:card|alert|badge|btn|table|space-y|p-\d+|text-|bg-|rounded|shadow)/.test(trimmed)) return true;
846
- // Count HTML tags
847
- const tagCount = (trimmed.match(/<[a-z][^>]*>/gi) || []).length;
848
- if (tagCount >= 2) return true; // Lower threshold for HTML detection
849
- return false;
168
+ this.ws.on('error', (err) => {
169
+ console.error('[WS] Error:', err);
170
+ document.getElementById('connectionStatus').textContent = 'Error';
171
+ });
850
172
  }
851
173
 
852
- parseAndRenderContent(content) {
853
- const elements = [];
854
- if (typeof content !== 'string') return null;
855
-
856
- const htmlCodeBlockRegex = /```html\n([\s\S]*?)\n```/g;
857
- let lastIndex = 0;
858
- let match;
859
-
860
- while ((match = htmlCodeBlockRegex.exec(content)) !== null) {
861
- if (match.index > lastIndex) {
862
- const textBefore = content.substring(lastIndex, match.index);
863
- if (textBefore.trim()) {
864
- elements.push(this.renderTextOrHtml(textBefore));
865
- }
866
- }
867
- elements.push(this.createSandboxedHtml(match[1]));
868
- lastIndex = htmlCodeBlockRegex.lastIndex;
869
- }
870
-
871
- if (lastIndex < content.length) {
872
- const remaining = content.substring(lastIndex);
873
- if (remaining.trim()) {
874
- elements.push(this.renderTextOrHtml(remaining));
875
- }
174
+ handleEvent(event) {
175
+ if (event.type === 'message_created') {
176
+ this.addMessageToDisplay(event.message);
177
+ this.handleMessageReceived(event.message);
178
+ } else if (event.type === 'conversations_updated') {
179
+ this.fetchConversations().then(() => this.renderChatHistory());
876
180
  }
877
-
878
- return elements.length > 0 ? elements : null;
879
181
  }
880
182
 
881
- renderTextOrHtml(text) {
882
- if (this.looksLikeHtml(text)) {
883
- return this.createSandboxedHtml(text);
884
- }
885
-
886
- // CRITICAL FIX: Don't bundle all text into one bubble
887
- // Try splitting by paragraph breaks first (double newlines)
888
- let parts = text.split('\n\n').filter(p => p.trim());
889
-
890
- // If no paragraphs found, try splitting by single newlines
891
- // (handles imported messages that may not have proper paragraph breaks)
892
- if (parts.length === 1) {
893
- const singleNewlines = text.split('\n').filter(p => p.trim());
894
- // Only use single newlines if we get reasonable chunks (3+ non-empty lines)
895
- if (singleNewlines.length >= 3) {
896
- parts = singleNewlines;
897
- }
898
- }
899
-
900
- // If still just one part and it's very long (>500 chars), split by sentences
901
- if (parts.length === 1 && text.length > 500) {
902
- const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
903
- if (sentences.length > 1) {
904
- parts = sentences.map(s => s.trim()).filter(s => s);
905
- }
906
- }
907
-
908
- if (parts.length === 1) {
909
- // Single item - just one bubble
910
- const bubble = document.createElement('div');
911
- bubble.className = 'message-bubble';
912
- bubble.textContent = text;
913
- return bubble;
914
- }
915
-
916
- // Multiple parts - create separate bubbles for each
917
- const container = document.createElement('div');
918
- container.className = 'message-bubbles-container';
919
-
920
- for (const part of parts) {
921
- const bubble = document.createElement('div');
922
- bubble.className = 'message-bubble';
923
- bubble.textContent = part;
924
- container.appendChild(bubble);
925
- }
926
-
927
- return container;
928
- }
183
+ addMessageToDisplay(message) {
184
+ if (this.currentConversation && message.conversationId === this.currentConversation) {
185
+ const chatDiv = document.getElementById('chatMessages');
186
+ if (!chatDiv) return;
929
187
 
930
- createSandboxedHtml(rawHtml) {
931
- const wrap = document.createElement('div');
932
- wrap.className = 'html-block rendered-html';
933
- const content = document.createElement('div');
934
- content.className = 'html-content';
935
-
936
- // Get current theme to apply to HTML content
937
- const currentTheme = document.documentElement.getAttribute('data-theme') ||
938
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
939
-
940
- // CRITICAL: Inject theme-aware CSS to ensure text colors work in dark/light mode
941
- const themeCSS = currentTheme === 'dark'
942
- ? `<style>
943
- .html-content {
944
- color: #f8fafc;
945
- background: transparent;
946
- }
947
- .html-content p { color: #cbd5e1; }
948
- .html-content h1, .html-content h2, .html-content h3,
949
- .html-content h4, .html-content h5, .html-content h6 {
950
- color: #f8fafc;
951
- }
952
- .html-content a { color: #6366f1; }
953
- .html-content code { color: #c7d2fe; background: rgba(0,0,0,0.3); }
954
- .html-content pre { background: rgba(0,0,0,0.5); color: #e0e7ff; }
955
- .html-content table { border-color: #334155; }
956
- .html-content th { background: #1a202c; color: #f8fafc; }
957
- .html-content td { border-color: #334155; }
958
- .html-content blockquote { border-color: #334155; color: #cbd5e1; }
959
- .html-content ul, .html-content ol { color: #cbd5e1; }
960
- .html-content li { color: #cbd5e1; }
961
- </style>`
962
- : `<style>
963
- .html-content {
964
- color: #1d2129;
965
- background: transparent;
966
- }
967
- .html-content p { color: #475569; }
968
- .html-content h1, .html-content h2, .html-content h3,
969
- .html-content h4, .html-content h5, .html-content h6 {
970
- color: #1d2129;
971
- }
972
- .html-content a { color: #4f46e5; }
973
- .html-content code { color: #6366f1; background: rgba(99,102,241,0.1); }
974
- .html-content pre { background: #f3f4f6; color: #1d2129; }
975
- .html-content table { border-color: #e5e7eb; }
976
- .html-content th { background: #f9fafb; color: #1d2129; }
977
- .html-content td { border-color: #e5e7eb; }
978
- .html-content blockquote { border-color: #e5e7eb; color: #475569; }
979
- .html-content ul, .html-content ol { color: #475569; }
980
- .html-content li { color: #475569; }
981
- </style>`;
982
-
983
- // CRITICAL: Ensure RippleUI styles are available for agent HTML
984
- // Agent responses use RippleUI/Tailwind classes, so wrap in a context that has those styles
985
- let enhancedHtml = themeCSS + rawHtml;
986
-
987
- // If HTML doesn't already have the RippleUI wrapper classes, add them
988
- if (!rawHtml.includes('space-y-4') && !rawHtml.includes('card') && !rawHtml.includes('alert')) {
989
- // Wrap in RippleUI container if agent didn't already wrap it
990
- enhancedHtml = themeCSS + `<div class="space-y-4 p-6 max-w-4xl">${rawHtml}</div>`;
991
- console.log('[HTML] Wrapped agent HTML in RippleUI container with theme CSS');
992
- } else {
993
- console.log('[HTML] Agent HTML already has RippleUI classes, applying theme CSS');
994
- }
995
-
996
- content.innerHTML = this.sanitizeHtml(enhancedHtml);
997
- wrap.appendChild(content);
998
-
999
- // Apply theme attribute to content so nested elements inherit
1000
- content.setAttribute('data-theme', currentTheme);
1001
-
1002
- return wrap;
1003
- }
188
+ const msgEl = document.createElement('div');
189
+ msgEl.className = `message ${message.role}`;
1004
190
 
1005
- addMessageToDisplay(msg) {
1006
- const div = document.getElementById('chatMessages');
1007
- if (!div) return;
1008
- const el = document.createElement('div');
1009
- el.className = `message ${msg.role}`;
1010
- el.dataset.messageId = msg.id;
1011
-
1012
- // CRITICAL: Always check for HTML content first - NEVER render HTML as plain text
1013
- if (typeof msg.content === 'string') {
1014
- // MANDATORY HTML RENDERING: Check if this is HTML before falling back to text
1015
- if (this.looksLikeHtml(msg.content)) {
1016
- console.log('[HTML] Agent response contains HTML - rendering as HTML');
1017
- el.appendChild(this.createSandboxedHtml(msg.content));
1018
- } else {
1019
- // Only use text rendering if it's not HTML
1020
- const parsed = this.parseAndRenderContent(msg.content);
1021
- if (parsed) {
1022
- parsed.forEach(elem => el.appendChild(elem));
191
+ // Try to parse content as JSON for structured display
192
+ let contentHtml = '';
193
+ try {
194
+ const parsed = typeof message.content === 'string' ? JSON.parse(message.content) : message.content;
195
+ if (parsed && parsed.type === 'claude_execution' && parsed.blocks) {
196
+ // Render each block with appropriate formatting
197
+ contentHtml = '<div class="execution-blocks">';
198
+ for (const block of parsed.blocks) {
199
+ contentHtml += this.renderMessageBlock(block);
200
+ }
201
+ contentHtml += '</div>';
1023
202
  } else {
1024
- const bubble = document.createElement('div');
1025
- bubble.className = 'message-bubble';
1026
- bubble.textContent = msg.content;
1027
- el.appendChild(bubble);
203
+ throw new Error('Not a claude_execution message');
1028
204
  }
205
+ } catch (e) {
206
+ // Fallback: render as plain text
207
+ const text = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
208
+ contentHtml = `<div class="message-content">${this.escapeHtml(text)}</div>`;
1029
209
  }
1030
- } else if (typeof msg.content === 'object' && msg.content !== null) {
1031
- // CRITICAL: Check for HTML content in object
1032
- let hasHtmlContent = false;
1033
-
1034
- // Display blocks if available (HTML blocks MUST be rendered as HTML)
1035
- if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
1036
- msg.content.blocks.forEach(block => {
1037
- if (block.type === 'html') {
1038
- console.log('[HTML] Rendering HTML block from agent');
1039
- const htmlEl = this.createHtmlBlock(block);
1040
- el.appendChild(htmlEl);
1041
- hasHtmlContent = true;
1042
- } else if (block.type === 'image') {
1043
- const imgEl = this.createImageBlock(block);
1044
- el.appendChild(imgEl);
1045
- hasHtmlContent = true;
1046
- }
1047
- });
1048
- }
1049
-
1050
- // CRITICAL: Agent responses are now HTML from system prompt
1051
- // Check if we have text first (which should be HTML)
1052
- if (msg.content.text && !hasHtmlContent) {
1053
- // ALWAYS check if text itself contains HTML first
1054
- if (this.looksLikeHtml(msg.content.text)) {
1055
- console.log('[HTML] ✅ Agent response is HTML - rendering directly (NOT segmenting)');
1056
- el.appendChild(this.createSandboxedHtml(msg.content.text));
1057
- } else {
1058
- // Only if NOT HTML, then try segmenting
1059
- console.log('[HTML] Text is not HTML, attempting segmentation');
1060
- if (msg.content.segments && Array.isArray(msg.content.segments)) {
1061
- console.log('[HTML] Rendering', msg.content.segments.length, 'segments');
1062
- msg.content.segments.forEach(segment => {
1063
- el.appendChild(this.renderSegment(segment));
1064
- });
1065
- } else {
1066
- const parsed = this.parseAndRenderContent(msg.content.text);
1067
- if (parsed) {
1068
- parsed.forEach(elem => el.appendChild(elem));
1069
- } else {
1070
- const bubble = document.createElement('div');
1071
- bubble.className = 'message-bubble';
1072
- bubble.textContent = msg.content.text;
1073
- el.appendChild(bubble);
1074
- }
1075
- }
1076
- }
1077
- } else if (msg.content.segments && Array.isArray(msg.content.segments) && !hasHtmlContent) {
1078
- // Fallback: only use segments if we have them and no text
1079
- console.log('[HTML] No text content, rendering segments');
1080
- msg.content.segments.forEach(segment => {
1081
- el.appendChild(this.renderSegment(segment));
1082
- });
1083
- }
1084
210
 
1085
- // Display metadata if available
1086
- if (msg.content.metadata) {
1087
- const metadataEl = this.renderMetadata(msg.content.metadata);
1088
- if (metadataEl) el.appendChild(metadataEl);
1089
- }
1090
- } else {
1091
- // Fallback for non-string, non-object content
1092
- const bubble = document.createElement('div');
1093
- bubble.className = 'message-bubble';
1094
- // Handle all object types: convert to string safely
1095
- if (typeof msg.content === 'object' && msg.content !== null) {
1096
- try {
1097
- bubble.textContent = JSON.stringify(msg.content, null, 2);
1098
- } catch (e) {
1099
- // If stringify fails (circular ref, etc), use toString
1100
- bubble.textContent = String(msg.content);
1101
- }
1102
- } else {
1103
- bubble.textContent = String(msg.content);
1104
- }
1105
- el.appendChild(bubble);
211
+ msgEl.innerHTML = contentHtml;
212
+ chatDiv.appendChild(msgEl);
213
+ chatDiv.scrollTop = chatDiv.scrollHeight;
1106
214
  }
1107
-
1108
- div.appendChild(el);
1109
215
  }
1110
216
 
1111
- renderSegment(segment) {
1112
- const el = document.createElement('div');
1113
- el.className = `segment segment-${segment.type}`;
217
+ renderMessageBlock(block) {
218
+ if (!block) return '';
1114
219
 
1115
- if (segment.type === 'code') {
1116
- const pre = document.createElement('pre');
1117
- pre.className = `code-block language-${segment.language || 'text'}`;
1118
- const code = document.createElement('code');
1119
- code.textContent = segment.content;
1120
- pre.appendChild(code);
1121
- el.appendChild(pre);
1122
- } else if (segment.type === 'heading') {
1123
- const tag = `h${Math.min(segment.level, 6)}`;
1124
- const heading = document.createElement(tag);
1125
- heading.className = 'response-heading';
1126
- heading.textContent = segment.content;
1127
- el.appendChild(heading);
1128
- } else if (segment.type === 'blockquote') {
1129
- const quote = document.createElement('blockquote');
1130
- quote.className = 'response-quote';
1131
- quote.textContent = segment.content;
1132
- el.appendChild(quote);
1133
- } else if (segment.type === 'list_item') {
1134
- const li = document.createElement('li');
1135
- li.className = 'response-list-item';
1136
- li.textContent = segment.content;
1137
- el.appendChild(li);
1138
- } else if (segment.type === 'thinking') {
1139
- // Collapsible thinking block
1140
- const details = document.createElement('details');
1141
- details.className = 'segment-thinking';
1142
- const summary = document.createElement('summary');
1143
- summary.textContent = '💭 Thinking';
1144
- details.appendChild(summary);
1145
- const content = document.createElement('div');
1146
- content.className = 'thinking-content';
1147
- content.textContent = segment.text;
1148
- details.appendChild(content);
1149
- el.appendChild(details);
1150
- } else if (segment.type === 'tool_use') {
1151
- // Tool call highlight
1152
- const div = document.createElement('div');
1153
- div.className = 'segment-tool-use';
1154
- div.innerHTML = `<div class="tool-icon">⚙️ Tool Call</div><pre class="tool-content"><code>${this.escapeHtml(segment.text)}</code></pre>`;
1155
- el.appendChild(div);
1156
- } else if (segment.type === 'tool_result') {
1157
- // Tool result
1158
- const div = document.createElement('div');
1159
- div.className = 'segment-tool-result';
1160
- div.innerHTML = `<div class="result-icon">📦 Result</div><pre class="result-content"><code>${this.escapeHtml(segment.text)}</code></pre>`;
1161
- el.appendChild(div);
1162
- } else if (segment.type === 'action') {
1163
- // Action statement - bold and prominent
1164
- const p = document.createElement('p');
1165
- p.className = 'response-action';
1166
- p.innerHTML = `<strong>→ ${this.escapeHtml(segment.text)}</strong>`;
1167
- el.appendChild(p);
1168
- } else if (segment.type === 'analysis') {
1169
- // Analysis/investigation
1170
- const p = document.createElement('p');
1171
- p.className = 'response-analysis';
1172
- p.innerHTML = `<em>🔍 ${this.escapeHtml(segment.text)}</em>`;
1173
- el.appendChild(p);
1174
- } else if (segment.type === 'result') {
1175
- // Result presentation
1176
- const div = document.createElement('div');
1177
- div.className = 'response-result';
1178
- div.innerHTML = segment.text
1179
- .replace(/&/g, '&amp;')
1180
- .replace(/</g, '&lt;')
1181
- .replace(/>/g, '&gt;')
1182
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1183
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
1184
- .replace(/`([^`]+)`/g, '<code>$1</code>');
1185
- el.appendChild(div);
1186
- } else if (segment.type === 'text') {
1187
- const p = document.createElement('p');
1188
- p.className = 'response-text';
1189
- p.innerHTML = segment.content
1190
- .replace(/&/g, '&amp;')
1191
- .replace(/</g, '&lt;')
1192
- .replace(/>/g, '&gt;')
1193
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1194
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
1195
- .replace(/`([^`]+)`/g, '<code>$1</code>');
1196
- el.appendChild(p);
1197
- }
1198
-
1199
- return el;
1200
- }
220
+ let html = '<div class="message-block">';
1201
221
 
1202
- escapeHtml(text) {
1203
- if (typeof text !== 'string') return '';
1204
- return text
1205
- .replace(/&/g, '&amp;')
1206
- .replace(/</g, '&lt;')
1207
- .replace(/>/g, '&gt;')
1208
- .replace(/"/g, '&quot;')
1209
- .replace(/'/g, '&#039;');
1210
- }
222
+ switch (block.type) {
223
+ case 'text':
224
+ html += `<div class="block-text">${this.escapeHtml(block.text || '')}</div>`;
225
+ break;
1211
226
 
1212
- renderMetadata(metadata) {
1213
- if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
1214
- return null;
1215
- }
227
+ case 'tool_use':
228
+ html += `<div class="block-tool-use">`;
229
+ html += `<strong class="tool-name">${this.escapeHtml(block.name || 'Tool')}</strong>`;
230
+ html += `<div class="tool-input"><pre>${this.escapeHtml(JSON.stringify(block.input || {}, null, 2))}</pre></div>`;
231
+ html += `</div>`;
232
+ break;
1216
233
 
1217
- const container = document.createElement('div');
1218
- container.className = 'response-metadata';
234
+ case 'tool_result':
235
+ html += `<div class="block-tool-result">`;
236
+ html += `<strong>Result:</strong>`;
237
+ const resultText = typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2);
238
+ html += `<div class="tool-result"><pre>${this.escapeHtml(resultText)}</pre></div>`;
239
+ html += `</div>`;
240
+ break;
1219
241
 
1220
- if (metadata.tools && metadata.tools.length > 0) {
1221
- const section = document.createElement('div');
1222
- section.className = 'metadata-section tools';
1223
- const title = document.createElement('strong');
1224
- title.textContent = 'Tools Used:';
1225
- section.appendChild(title);
1226
- const ul = document.createElement('ul');
1227
- metadata.tools.forEach(tool => {
1228
- const li = document.createElement('li');
1229
- const code = document.createElement('code');
1230
- code.textContent = tool.name;
1231
- li.appendChild(code);
1232
- if (tool.description) {
1233
- li.appendChild(document.createTextNode(`: ${tool.description}`));
242
+ case 'file_operation':
243
+ html += `<div class="block-file-op">`;
244
+ html += `<strong class="file-action">${this.escapeHtml(block.action || 'File Operation')}</strong>`;
245
+ html += `<div class="file-path">${this.escapeHtml(block.path || '')}</div>`;
246
+ if (block.content) {
247
+ html += `<div class="file-content"><pre>${this.escapeHtml(block.content.substring(0, 500))}</pre></div>`;
1234
248
  }
1235
- ul.appendChild(li);
1236
- });
1237
- section.appendChild(ul);
1238
- container.appendChild(section);
1239
- }
1240
-
1241
- if (metadata.thinking && metadata.thinking.length > 0) {
1242
- const section = document.createElement('details');
1243
- section.className = 'metadata-section thinking';
1244
- const summary = document.createElement('summary');
1245
- summary.textContent = 'Reasoning';
1246
- section.appendChild(summary);
1247
- metadata.thinking.forEach(thought => {
1248
- const p = document.createElement('p');
1249
- p.textContent = thought;
1250
- section.appendChild(p);
1251
- });
1252
- container.appendChild(section);
1253
- }
1254
-
1255
- if (metadata.subagents && metadata.subagents.length > 0) {
1256
- const section = document.createElement('div');
1257
- section.className = 'metadata-section subagents';
1258
- const title = document.createElement('strong');
1259
- title.textContent = 'Subagents:';
1260
- section.appendChild(title);
1261
- const ul = document.createElement('ul');
1262
- metadata.subagents.forEach(agent => {
1263
- const li = document.createElement('li');
1264
- li.textContent = agent;
1265
- ul.appendChild(li);
1266
- });
1267
- section.appendChild(ul);
1268
- container.appendChild(section);
1269
- }
249
+ html += `</div>`;
250
+ break;
1270
251
 
1271
- if (metadata.tasks && metadata.tasks.length > 0) {
1272
- const section = document.createElement('div');
1273
- section.className = 'metadata-section tasks';
1274
- const title = document.createElement('strong');
1275
- title.textContent = 'Tasks:';
1276
- section.appendChild(title);
1277
- const ul = document.createElement('ul');
1278
- metadata.tasks.forEach(task => {
1279
- const li = document.createElement('li');
1280
- li.textContent = task;
1281
- ul.appendChild(li);
1282
- });
1283
- section.appendChild(ul);
1284
- container.appendChild(section);
252
+ default:
253
+ html += `<div class="block-unknown">${this.escapeHtml(JSON.stringify(block, null, 2))}</div>`;
1285
254
  }
1286
255
 
1287
- return container;
256
+ html += '</div>';
257
+ return html;
1288
258
  }
1289
259
 
1290
- async startNewChat(folderPath) {
1291
- if (!this.selectedAgent) {
1292
- const firstAgent = Array.from(this.agents.keys())[0];
1293
- if (firstAgent) {
1294
- this.selectedAgent = firstAgent;
1295
- }
1296
- }
1297
- const title = folderPath
1298
- ? folderPath.split('/').pop() || folderPath
1299
- : `Chat ${this.conversations.size + 1}`;
1300
- try {
1301
- const res = await this.apiFetch(BASE_URL + '/api/conversations', {
1302
- method: 'POST',
1303
- headers: { 'Content-Type': 'application/json' },
1304
- body: JSON.stringify({ agentId: this.selectedAgent || 'claude-code', title }),
1305
- });
1306
- const data = await res.json();
1307
- if (data.conversation) {
1308
- const conv = data.conversation;
1309
- if (folderPath) conv.folderPath = folderPath;
1310
- this.conversations.set(conv.id, conv);
1311
- this.currentConversation = conv.id;
1312
- this.renderChatHistory();
1313
- this.displayConversation(conv.id);
1314
- }
1315
- } catch (e) {
1316
- console.error('startNewChat:', e);
260
+ handleMessageReceived(message) {
261
+ if (message.role === 'user') {
262
+ document.getElementById('messageInput').value = '';
263
+ document.getElementById('messageInput').focus();
1317
264
  }
1318
265
  }
1319
266
 
1320
267
  async sendMessage() {
1321
268
  const input = document.getElementById('messageInput');
1322
- const message = input.value.trim();
1323
- if (!message) return;
1324
- if (!this.selectedAgent) {
1325
- this.addSystemMessage('Please select an agent first');
1326
- return;
1327
- }
1328
- if (!this.currentConversation) {
1329
- await this.startNewChat();
1330
- }
1331
- if (!this.currentConversation) return;
1332
- const conv = this.conversations.get(this.currentConversation);
269
+ const content = input.value.trim();
1333
270
 
1334
- const idempotencyKey = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1335
- const tempId = `pending-${idempotencyKey}`;
1336
- this.addMessageToDisplay({ role: 'user', content: message, id: tempId });
1337
- input.value = '';
1338
- this.updateSendButtonState();
271
+ if (!content || !this.currentConversation || !this.selectedAgent) return;
1339
272
 
1340
273
  try {
1341
- const folderPath = conv?.folderPath || localStorage.getItem('gmgui-home') || '/config';
1342
- const res = await this.apiFetch(`${BASE_URL}/api/conversations/${this.currentConversation}/messages`, {
274
+ const res = await fetch(BASE_URL + `/api/conversations/${this.currentConversation}/messages`, {
1343
275
  method: 'POST',
1344
276
  headers: { 'Content-Type': 'application/json' },
1345
277
  body: JSON.stringify({
1346
- content: message,
1347
- agentId: this.selectedAgent,
1348
- folderContext: { path: folderPath, isFolder: true },
1349
- idempotencyKey,
1350
- }),
278
+ content,
279
+ agentId: this.selectedAgent
280
+ })
1351
281
  });
1352
- if (!res.ok) {
1353
- const err = await res.json();
1354
- this.addMessageToDisplay({ role: 'system', content: `Error: ${err.error || 'Request failed'}` });
1355
- return;
282
+
283
+ if (res.ok) {
284
+ input.value = '';
1356
285
  }
1357
- const data = await res.json();
1358
- const optimisticEl = document.querySelector(`[data-message-id="${tempId}"]`);
1359
- if (optimisticEl) optimisticEl.dataset.messageId = data.message.id;
1360
- this.idempotencyKeys.set(idempotencyKey, data.session.id);
1361
- this.startPollingMessages(this.currentConversation);
1362
286
  } catch (e) {
1363
- this.addMessageToDisplay({ role: 'system', content: `Error: ${e.message}` });
1364
- }
1365
- if (this.settings.autoScroll) {
1366
- const div = document.getElementById('chatMessages');
1367
- if (div) div.scrollTop = div.scrollHeight;
287
+ console.error('[APP] Error sending message:', e);
1368
288
  }
1369
289
  }
1370
290
 
1371
- addSystemMessage(text) {
1372
- this.addMessageToDisplay({ role: 'system', content: text });
1373
- }
291
+ async createConversation() {
292
+ const title = document.getElementById('newConvTitle')?.value || 'New Conversation';
1374
293
 
1375
- stopPollingMessages() {
1376
- if (this.pollingInterval) {
1377
- clearInterval(this.pollingInterval);
1378
- this.pollingInterval = null;
1379
- console.log('[POLLING] Polling stopped - using WebSocket push instead');
1380
- }
1381
- }
1382
-
1383
- startPollingMessages(conversationId) {
1384
- // DEPRECATED: Polling mechanism replaced with WebSocket push
1385
- // This method is kept for backwards compatibility but does nothing
1386
- // Messages now arrive via WebSocket in real-time via handleSyncEvent
1387
- console.log('[POLLING] Polling requested but disabled - WebSocket handles real-time updates');
1388
- this.stopPollingMessages();
1389
- }
1390
-
1391
- createThoughtBlock() {
1392
- const wrap = document.createElement('div');
1393
- wrap.className = 'thought-block';
1394
- const header = document.createElement('div');
1395
- header.className = 'thought-header';
1396
- header.textContent = 'Thinking...';
1397
- header.onclick = () => wrap.classList.toggle('collapsed');
1398
- const content = document.createElement('div');
1399
- content.className = 'thought-content';
1400
- wrap.appendChild(header);
1401
- wrap.appendChild(content);
1402
- return wrap;
1403
- }
1404
-
1405
- createToolBlock(event) {
1406
- const wrap = document.createElement('div');
1407
- wrap.className = `tool-block status-${event.status || 'running'}`;
1408
- const header = document.createElement('div');
1409
- header.className = 'tool-header';
1410
- const kindIcons = { execute: '>', read: '?', edit: '/', search: '~', fetch: '@', write: '/', think: '!', other: '#' };
1411
- const icon = kindIcons[event.kind] || '#';
1412
- header.innerHTML = `<span class="tool-icon">${escapeHtml(icon)}</span><span class="tool-title">${escapeHtml(event.title || event.kind || 'tool')}</span><span class="tool-status">${escapeHtml(event.status || 'running')}</span>`;
1413
- header.onclick = () => wrap.classList.toggle('collapsed');
1414
- wrap.appendChild(header);
1415
- if (event.content && event.content.length) {
1416
- const body = document.createElement('div');
1417
- body.className = 'tool-body';
1418
- event.content.forEach(c => {
1419
- if (c.text) body.textContent += c.text;
294
+ try {
295
+ const res = await fetch(BASE_URL + '/api/conversations', {
296
+ method: 'POST',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify({ title, agentId: this.selectedAgent })
1420
299
  });
1421
- wrap.appendChild(body);
1422
- }
1423
- return wrap;
1424
- }
1425
300
 
1426
- updateToolBlock(block, event) {
1427
- block.className = `tool-block status-${event.status || 'completed'}`;
1428
- const statusEl = block.querySelector('.tool-status');
1429
- if (statusEl) statusEl.textContent = event.status || 'completed';
1430
- if (event.content && event.content.length) {
1431
- let body = block.querySelector('.tool-body');
1432
- if (!body) { body = document.createElement('div'); body.className = 'tool-body'; block.appendChild(body); }
1433
- event.content.forEach(c => {
1434
- if (c.text) body.textContent += c.text;
1435
- });
301
+ if (res.ok) {
302
+ await this.fetchConversations();
303
+ this.renderChatHistory();
304
+ }
305
+ } catch (e) {
306
+ console.error('[APP] Error creating conversation:', e);
1436
307
  }
1437
308
  }
1438
309
 
1439
- createPlanBlock(entries) {
1440
- const wrap = document.createElement('div');
1441
- wrap.className = 'plan-block';
1442
- const header = document.createElement('div');
1443
- header.className = 'plan-header';
1444
- header.textContent = 'Plan';
1445
- wrap.appendChild(header);
1446
- if (entries && entries.length) {
1447
- entries.forEach(entry => {
1448
- const item = document.createElement('div');
1449
- item.className = 'plan-item';
1450
- item.textContent = entry.title || entry.description || JSON.stringify(entry);
1451
- wrap.appendChild(item);
1452
- });
1453
- }
1454
- return wrap;
310
+ selectConversation(convId) {
311
+ this.currentConversation = convId;
312
+ document.querySelectorAll('.chat-item').forEach(el => el.classList.remove('active'));
313
+ const el = document.querySelector(`[data-conv-id="${convId}"]`);
314
+ if (el) el.classList.add('active');
315
+ this.renderChatMessages();
1455
316
  }
1456
317
 
1457
- createHtmlBlock(event) {
1458
- const wrap = document.createElement('div');
1459
- wrap.className = 'html-block rendered-html';
1460
- if (event.id) wrap.id = `html-${event.id}`;
1461
- if (event.title) {
1462
- const header = document.createElement('div');
1463
- header.className = 'html-header';
1464
- header.textContent = event.title;
1465
- wrap.appendChild(header);
1466
- }
1467
- const content = document.createElement('div');
1468
- content.className = 'html-content';
318
+ async renderChatMessages() {
319
+ const chatDiv = document.getElementById('chatMessages');
320
+ if (!chatDiv || !this.currentConversation) return;
1469
321
 
1470
- // Get current theme to apply to HTML content
1471
- const currentTheme = document.documentElement.getAttribute('data-theme') ||
1472
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
322
+ chatDiv.innerHTML = '';
323
+ const messages = await this.fetchMessages(this.currentConversation);
324
+ for (const msg of messages) {
325
+ const msgEl = document.createElement('div');
326
+ msgEl.className = `message ${msg.role}`;
1473
327
 
1474
- // Apply theme-aware CSS for consistent colors in dark/light mode
1475
- const themeCSS = currentTheme === 'dark'
1476
- ? `<style>
1477
- .html-content {
1478
- color: #f8fafc;
1479
- background: transparent;
1480
- }
1481
- .html-content p { color: #cbd5e1; }
1482
- .html-content h1, .html-content h2, .html-content h3,
1483
- .html-content h4, .html-content h5, .html-content h6 {
1484
- color: #f8fafc;
1485
- }
1486
- .html-content a { color: #6366f1; }
1487
- .html-content code { color: #c7d2fe; background: rgba(0,0,0,0.3); }
1488
- .html-content pre { background: rgba(0,0,0,0.5); color: #e0e7ff; }
1489
- .html-content table { border-color: #334155; }
1490
- .html-content th { background: #1a202c; color: #f8fafc; }
1491
- .html-content td { border-color: #334155; }
1492
- .html-content blockquote { border-color: #334155; color: #cbd5e1; }
1493
- .html-content ul, .html-content ol { color: #cbd5e1; }
1494
- .html-content li { color: #cbd5e1; }
1495
- </style>`
1496
- : `<style>
1497
- .html-content {
1498
- color: #1d2129;
1499
- background: transparent;
1500
- }
1501
- .html-content p { color: #475569; }
1502
- .html-content h1, .html-content h2, .html-content h3,
1503
- .html-content h4, .html-content h5, .html-content h6 {
1504
- color: #1d2129;
1505
- }
1506
- .html-content a { color: #4f46e5; }
1507
- .html-content code { color: #6366f1; background: rgba(99,102,241,0.1); }
1508
- .html-content pre { background: #f3f4f6; color: #1d2129; }
1509
- .html-content table { border-color: #e5e7eb; }
1510
- .html-content th { background: #f9fafb; color: #1d2129; }
1511
- .html-content td { border-color: #e5e7eb; }
1512
- .html-content blockquote { border-color: #e5e7eb; color: #475569; }
1513
- .html-content ul, .html-content ol { color: #475569; }
1514
- .html-content li { color: #475569; }
1515
- </style>`;
1516
-
1517
- const enhancedHtml = themeCSS + this.sanitizeHtml(event.html);
1518
- content.innerHTML = enhancedHtml;
1519
- content.setAttribute('data-theme', currentTheme);
1520
- wrap.appendChild(content);
1521
- return wrap;
1522
- }
328
+ // Try to parse content as JSON for structured display
329
+ let contentHtml = '';
330
+ try {
331
+ const parsed = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content;
332
+ if (parsed && parsed.type === 'claude_execution' && parsed.blocks) {
333
+ // Render each block with appropriate formatting
334
+ contentHtml = '<div class="execution-blocks">';
335
+ for (const block of parsed.blocks) {
336
+ contentHtml += this.renderMessageBlock(block);
337
+ }
338
+ contentHtml += '</div>';
339
+ } else {
340
+ throw new Error('Not a claude_execution message');
341
+ }
342
+ } catch (e) {
343
+ // Fallback: render as plain text
344
+ const text = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
345
+ contentHtml = `<div class="message-content">${this.escapeHtml(text)}</div>`;
346
+ }
1523
347
 
1524
- createImageBlock(event) {
1525
- const wrap = document.createElement('div');
1526
- wrap.className = 'image-block';
1527
- if (event.title) {
1528
- const header = document.createElement('div');
1529
- header.className = 'image-header';
1530
- header.textContent = event.title;
1531
- wrap.appendChild(header);
348
+ msgEl.innerHTML = contentHtml;
349
+ chatDiv.appendChild(msgEl);
1532
350
  }
1533
- const img = document.createElement('img');
1534
- img.src = event.url;
1535
- img.alt = event.alt || 'Image from agent';
1536
- img.className = 'image-content';
1537
- img.style.maxWidth = '100%';
1538
- img.style.height = 'auto';
1539
- img.style.borderRadius = '0.25rem';
1540
- wrap.appendChild(img);
1541
- return wrap;
1542
- }
1543
-
1544
- updateSendButtonState() {
1545
- const input = document.getElementById('messageInput');
1546
- const sendBtn = document.getElementById('sendBtn');
1547
- if (sendBtn) sendBtn.disabled = !input || !input.value.trim();
1548
351
  }
1549
352
 
1550
- openFolderBrowser() {
1551
- const dlgModal = document.getElementById('folderBrowserModal');
1552
- if (!dlgModal) return;
1553
- const pathInput = document.getElementById('folderPath');
1554
- pathInput.value = '~/';
1555
- this.loadFolderContents(this.expandHome('~/'));
1556
- dlgModal.classList.add('active');
1557
- }
353
+ renderChatHistory() {
354
+ const list = document.getElementById('chatList');
355
+ if (!list) return;
1558
356
 
1559
- closeFolderBrowser() {
1560
- const dlgModal = document.getElementById('folderBrowserModal');
1561
- if (dlgModal) dlgModal.classList.remove('active');
1562
- }
357
+ list.innerHTML = '';
358
+ const convs = Array.from(this.conversations.values())
359
+ .sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
1563
360
 
1564
- async loadFolderContents(folderPath) {
1565
- const list = document.getElementById('folderBrowserList');
1566
- if (!list) return;
1567
- list.innerHTML = '<div style="padding: 1rem; color: var(--text-tertiary);">Loading...</div>';
1568
- try {
1569
- const res = await this.apiFetch(BASE_URL + '/api/folders', {
1570
- method: 'POST',
1571
- headers: { 'Content-Type': 'application/json' },
1572
- body: JSON.stringify({ path: folderPath }),
1573
- });
1574
- if (res.ok) {
1575
- const data = await res.json();
1576
- this.renderFolderList(data.folders, folderPath);
1577
- } else {
1578
- list.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">Error loading folder</div>';
1579
- }
1580
- } catch (e) {
1581
- list.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">Error: ' + e.message + '</div>';
361
+ for (const conv of convs) {
362
+ const el = document.createElement('div');
363
+ el.className = 'chat-item';
364
+ el.setAttribute('data-conv-id', conv.id);
365
+ el.innerHTML = `<div class="chat-item-title">${this.escapeHtml(conv.title || 'Untitled')}</div>`;
366
+ el.onclick = () => this.selectConversation(conv.id);
367
+ list.appendChild(el);
1582
368
  }
1583
369
  }
1584
370
 
1585
- renderFolderList(folders, currentPath) {
1586
- const list = document.getElementById('folderBrowserList');
1587
- if (!list) return;
1588
- list.innerHTML = '';
1589
- if (currentPath !== '/' && currentPath !== '/root') {
1590
- const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
1591
- const parentItem = document.createElement('div');
1592
- parentItem.className = 'folder-item';
1593
- parentItem.style.cssText = 'padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid var(--border-color);';
1594
- parentItem.innerHTML = '<span>../</span>';
1595
- parentItem.onclick = () => {
1596
- document.getElementById('folderPath').value = parentPath;
1597
- this.loadFolderContents(parentPath);
1598
- };
1599
- list.appendChild(parentItem);
1600
- }
1601
- if (!folders || folders.length === 0) {
1602
- const empty = document.createElement('div');
1603
- empty.style.cssText = 'padding: 1rem; color: var(--text-tertiary); text-align: center;';
1604
- empty.textContent = 'No subfolders found';
1605
- list.appendChild(empty);
1606
- return;
371
+ renderAll() {
372
+ this.renderChatHistory();
373
+ if (this.conversations.size > 0 && !this.currentConversation) {
374
+ const firstConv = Array.from(this.conversations.values())[0];
375
+ this.selectConversation(firstConv.id);
1607
376
  }
1608
- folders.forEach(folder => {
1609
- const item = document.createElement('div');
1610
- item.style.cssText = 'padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid var(--border-color);';
1611
- item.textContent = folder.name;
1612
- item.onclick = () => {
1613
- const newPath = currentPath === '/' ? '/' + folder.name : currentPath + '/' + folder.name;
1614
- document.getElementById('folderPath').value = newPath;
1615
- this.loadFolderContents(newPath);
1616
- };
1617
- list.appendChild(item);
1618
- });
1619
377
  }
1620
- }
1621
-
1622
- function escapeHtml(text) {
1623
- const div = document.createElement('div');
1624
- div.textContent = text;
1625
- return div.innerHTML;
1626
- }
1627
-
1628
- function showNewChatModal() {
1629
- const dlgModal = document.getElementById('newChatModal');
1630
- if (dlgModal) dlgModal.classList.add('active');
1631
- }
1632
-
1633
- function closeNewChatModal() {
1634
- const dlgModal = document.getElementById('newChatModal');
1635
- if (dlgModal) dlgModal.classList.remove('active');
1636
- }
1637
-
1638
- function createChatInWorkspace() {
1639
- closeNewChatModal();
1640
- app.startNewChat();
1641
- }
1642
378
 
1643
- function createChatInFolder() {
1644
- closeNewChatModal();
1645
- app.openFolderBrowser();
1646
- }
1647
-
1648
- async function importClaudeCodeConversations() {
1649
- closeNewChatModal();
1650
- try {
1651
- const res = await this.apiFetch(BASE_URL + '/api/import/claude-code');
1652
- const data = await res.json();
1653
-
1654
- if (!data.imported) {
1655
- alert('No Claude Code conversations found to import.');
1656
- return;
379
+ setupEventListeners() {
380
+ const sendBtn = document.getElementById('sendBtn');
381
+ if (sendBtn) {
382
+ sendBtn.onclick = () => this.sendMessage();
1657
383
  }
1658
384
 
1659
- const imported = data.imported.filter(r => r.status === 'imported');
1660
- const skipped = data.imported.filter(r => r.status === 'skipped');
1661
- const errors = data.imported.filter(r => r.status === 'error');
1662
-
1663
- let message = `Import complete!\n\n`;
1664
- if (imported.length > 0) {
1665
- message += `✓ Imported: ${imported.length} conversation(s)\n`;
1666
- }
1667
- if (skipped.length > 0) {
1668
- message += `⊘ Skipped: ${skipped.length} (already imported)\n`;
1669
- }
1670
- if (errors.length > 0) {
1671
- message += `✗ Errors: ${errors.length}\n`;
385
+ const input = document.getElementById('messageInput');
386
+ if (input) {
387
+ input.addEventListener('keypress', (e) => {
388
+ if (e.key === 'Enter' && !e.shiftKey) {
389
+ e.preventDefault();
390
+ this.sendMessage();
391
+ }
392
+ });
1672
393
  }
1673
394
 
1674
- alert(message.trim());
1675
-
1676
- if (imported.length > 0) {
1677
- await app.fetchConversations();
1678
- app.renderAll();
395
+ const newConvBtn = document.getElementById('newConversationBtn');
396
+ if (newConvBtn) {
397
+ newConvBtn.onclick = () => this.createConversation();
1679
398
  }
1680
- } catch (e) {
1681
- console.error('Import error:', e);
1682
- alert('Failed to import Claude Code conversations: ' + e.message);
1683
399
  }
1684
- }
1685
-
1686
- function sendMessage() { app.sendMessage(); }
1687
-
1688
- function toggleSidebar() {
1689
- const sidebar = document.getElementById('sidebar');
1690
- if (sidebar) sidebar.classList.toggle('open');
1691
- }
1692
400
 
1693
- function switchTab(tabName) {
1694
- const panel = document.getElementById('settingsPanel');
1695
- const main = document.querySelector('.main-content');
1696
- if (tabName === 'settings' && panel && main) {
1697
- panel.style.display = 'flex';
1698
- main.style.display = 'none';
1699
- } else if (tabName === 'chat' && panel && main) {
1700
- panel.style.display = 'none';
1701
- main.style.display = 'flex';
401
+ escapeHtml(text) {
402
+ const div = document.createElement('div');
403
+ div.textContent = text;
404
+ return div.innerHTML;
1702
405
  }
1703
406
  }
1704
407
 
1705
- function closeFolderBrowser() { app.closeFolderBrowser(); }
408
+ const app = new GMGUIApp();
1706
409
 
1707
- function browseFolders() {
1708
- const pathInput = document.getElementById('folderPath');
1709
- const p = pathInput.value.trim() || '~/';
1710
- app.loadFolderContents(app.expandHome(p));
1711
- }
1712
-
1713
- function confirmFolderSelection() {
1714
- const pathInput = document.getElementById('folderPath');
1715
- const p = pathInput.value.trim();
1716
- if (!p) return;
1717
- app.startNewChat(app.expandHome(p));
1718
- app.closeFolderBrowser();
1719
- }
1720
-
1721
- // Wait for DOM to be fully ready before initializing
1722
410
  function initializeApp() {
1723
- try {
1724
- console.log('[DEBUG] initializeApp: Checking if DOM is ready');
1725
- const chatList = document.getElementById('chatList');
1726
- if (!chatList) {
1727
- console.warn('[DEBUG] initializeApp: chatList not found, waiting 100ms');
1728
- setTimeout(initializeApp, 100);
1729
- return;
1730
- }
1731
-
1732
- console.log('[DEBUG] initializeApp: DOM is ready, creating GMGUIApp');
1733
- try {
1734
- window.app = new GMGUIApp();
1735
- window._app = window.app;
1736
- console.log('[DEBUG] initializeApp: GMGUIApp constructor completed');
1737
- } catch (constructorError) {
1738
- console.error('[ERROR] GMGUIApp constructor failed:', constructorError.message);
1739
- console.error('[ERROR] Stack:', constructorError.stack);
1740
- throw constructorError;
1741
- }
1742
-
1743
- // Debug: Log app state to window for inspection
1744
- window._debug = {
1745
- get conversations() { return Array.from(window.app.conversations.values()).map(c => ({ id: c.id, title: c.title })); },
1746
- get conversationCount() { return window.app.conversations.size; },
1747
- get selectedAgent() { return window.app.selectedAgent; },
1748
- get currentConversation() { return window.app.currentConversation; },
1749
- checkChatListElement() { return document.getElementById('chatList'); },
1750
- checkChatListChildCount() { return document.getElementById('chatList')?.children?.length || 0; },
1751
- async forceRefetch() {
1752
- console.log('[FORCE] Forcing fetchConversations...');
1753
- await window.app.fetchConversations();
1754
- console.log('[FORCE] Conversations loaded:', window.app.conversations.size);
1755
- window.app.renderChatHistory();
1756
- console.log('[FORCE] renderChatHistory called');
1757
- return window.app.conversations.size;
1758
- }
1759
- };
1760
-
1761
- console.log('[DEBUG] initializeApp: GMGUIApp created successfully with', window.app.conversations.size, 'conversations');
1762
- } catch (error) {
1763
- console.error('[CRITICAL ERROR] initializeApp failed:', error.message);
1764
- console.error('[CRITICAL ERROR] Stack trace:', error.stack);
1765
-
1766
- // Show error on page
1767
- const chatList = document.getElementById('chatList');
1768
- if (chatList) {
1769
- chatList.innerHTML = `
1770
- <div style="color: red; padding: 1rem; font-family: monospace; font-size: 0.75rem;">
1771
- <strong>INITIALIZATION ERROR</strong><br>
1772
- ${error.message}<br>
1773
- <br>
1774
- Check browser console (F12) for details.
1775
- </div>
1776
- `;
1777
- }
1778
- }
411
+ app.init().catch(err => {
412
+ console.error('[CRITICAL] Failed to initialize app:', err);
413
+ });
1779
414
  }
1780
415
 
1781
- if (document.readyState === 'loading') {
1782
- document.addEventListener('DOMContentLoaded', initializeApp);
1783
- } else {
1784
- initializeApp();
416
+ function sendMessage() {
417
+ app.sendMessage();
1785
418
  }
419
+
420
+ window.addEventListener('load', initializeApp);