agentgui 1.0.64 → 1.0.66

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