agentgui 1.0.65 → 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/.prd +54 -0
- package/CLAUDE.md +176 -0
- package/lib/claude-runner.js +71 -0
- package/lib/database-service.ts +388 -0
- package/lib/machines.ts +593 -0
- package/lib/schemas.ts +213 -0
- package/lib/sync-service.ts +340 -0
- package/lib/types.ts +245 -0
- package/package.json +1 -1
- package/server.js +69 -277
- package/static/app.js +210 -1575
- package/static/styles.css +135 -0
- package/conversation-sync.js +0 -196
- package/state-manager.js +0 -360
- package/state-validator.js +0 -150
- package/static/sync-manager.js +0 -273
- package/stream-handler.js +0 -106
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.
|
|
87
|
-
this.
|
|
88
|
-
this.
|
|
89
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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('[
|
|
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
|
|
110
|
+
const res = await fetch(BASE_URL + '/api/agents');
|
|
556
111
|
const data = await res.json();
|
|
557
|
-
|
|
558
|
-
|
|
112
|
+
for (const agent of data.agents || []) {
|
|
113
|
+
this.agents.set(agent.id, agent);
|
|
559
114
|
}
|
|
560
115
|
} catch (e) {
|
|
561
|
-
console.error('
|
|
116
|
+
console.error('[APP] Error fetching agents:', e);
|
|
562
117
|
}
|
|
563
118
|
}
|
|
564
119
|
|
|
565
120
|
async fetchConversations() {
|
|
566
121
|
try {
|
|
567
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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('[
|
|
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
|
|
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('
|
|
140
|
+
console.error('[APP] Error fetching messages:', e);
|
|
611
141
|
return [];
|
|
612
142
|
}
|
|
613
143
|
}
|
|
614
144
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
this.
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
931
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
el.className = `segment segment-${segment.type}`;
|
|
217
|
+
renderMessageBlock(block) {
|
|
218
|
+
if (!block) return '';
|
|
1114
219
|
|
|
1115
|
-
|
|
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, '&')
|
|
1180
|
-
.replace(/</g, '<')
|
|
1181
|
-
.replace(/>/g, '>')
|
|
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, '&')
|
|
1191
|
-
.replace(/</g, '<')
|
|
1192
|
-
.replace(/>/g, '>')
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
.replace(/</g, '<')
|
|
1207
|
-
.replace(/>/g, '>')
|
|
1208
|
-
.replace(/"/g, '"')
|
|
1209
|
-
.replace(/'/g, ''');
|
|
1210
|
-
}
|
|
222
|
+
switch (block.type) {
|
|
223
|
+
case 'text':
|
|
224
|
+
html += `<div class="block-text">${this.escapeHtml(block.text || '')}</div>`;
|
|
225
|
+
break;
|
|
1211
226
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
256
|
+
html += '</div>';
|
|
257
|
+
return html;
|
|
1288
258
|
}
|
|
1289
259
|
|
|
1290
|
-
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1347
|
-
agentId: this.selectedAgent
|
|
1348
|
-
|
|
1349
|
-
idempotencyKey,
|
|
1350
|
-
}),
|
|
278
|
+
content,
|
|
279
|
+
agentId: this.selectedAgent
|
|
280
|
+
})
|
|
1351
281
|
});
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
}
|
|
291
|
+
async createConversation() {
|
|
292
|
+
const title = document.getElementById('newConvTitle')?.value || 'New Conversation';
|
|
1374
293
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1458
|
-
const
|
|
1459
|
-
|
|
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
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1551
|
-
const
|
|
1552
|
-
if (!
|
|
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
|
-
|
|
1560
|
-
const
|
|
1561
|
-
|
|
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
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
if (!
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1724
|
-
console.
|
|
1725
|
-
|
|
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
|
-
|
|
1782
|
-
|
|
1783
|
-
} else {
|
|
1784
|
-
initializeApp();
|
|
416
|
+
function sendMessage() {
|
|
417
|
+
app.sendMessage();
|
|
1785
418
|
}
|
|
419
|
+
|
|
420
|
+
window.addEventListener('load', initializeApp);
|