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/.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 +73 -279
- package/static/app.js +205 -1584
- 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,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.
|
|
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
|
-
}
|
|
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
|
|
110
|
+
const res = await fetch(BASE_URL + '/api/agents');
|
|
534
111
|
const data = await res.json();
|
|
535
|
-
|
|
536
|
-
|
|
112
|
+
for (const agent of data.agents || []) {
|
|
113
|
+
this.agents.set(agent.id, agent);
|
|
537
114
|
}
|
|
538
115
|
} catch (e) {
|
|
539
|
-
console.error('
|
|
116
|
+
console.error('[APP] Error fetching agents:', e);
|
|
540
117
|
}
|
|
541
118
|
}
|
|
542
119
|
|
|
543
120
|
async fetchConversations() {
|
|
544
121
|
try {
|
|
545
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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('[
|
|
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
|
|
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('
|
|
140
|
+
console.error('[APP] Error fetching messages:', e);
|
|
589
141
|
return [];
|
|
590
142
|
}
|
|
591
143
|
}
|
|
592
144
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
this.
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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, '&')
|
|
1158
|
-
.replace(/</g, '<')
|
|
1159
|
-
.replace(/>/g, '>')
|
|
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, '&')
|
|
1169
|
-
.replace(/</g, '<')
|
|
1170
|
-
.replace(/>/g, '>')
|
|
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
|
-
|
|
1178
|
-
}
|
|
220
|
+
let html = '<div class="message-block">';
|
|
1179
221
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
.replace(/</g, '<')
|
|
1185
|
-
.replace(/>/g, '>')
|
|
1186
|
-
.replace(/"/g, '"')
|
|
1187
|
-
.replace(/'/g, ''');
|
|
1188
|
-
}
|
|
222
|
+
switch (block.type) {
|
|
223
|
+
case 'text':
|
|
224
|
+
html += `<div class="block-text">${this.escapeHtml(block.text || '')}</div>`;
|
|
225
|
+
break;
|
|
1189
226
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
section.appendChild(ul);
|
|
1216
|
-
container.appendChild(section);
|
|
1217
|
-
}
|
|
249
|
+
html += `</div>`;
|
|
250
|
+
break;
|
|
1218
251
|
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
|
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({
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
content,
|
|
279
|
+
agentId: this.selectedAgent
|
|
280
|
+
})
|
|
1283
281
|
});
|
|
1284
|
-
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
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('
|
|
287
|
+
console.error('[APP] Error sending message:', e);
|
|
1295
288
|
}
|
|
1296
289
|
}
|
|
1297
290
|
|
|
1298
|
-
async
|
|
1299
|
-
const
|
|
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
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
this.
|
|
1333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1350
|
-
this.
|
|
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
|
-
|
|
1354
|
-
|
|
318
|
+
async renderChatMessages() {
|
|
319
|
+
const chatDiv = document.getElementById('chatMessages');
|
|
320
|
+
if (!chatDiv || !this.currentConversation) return;
|
|
1355
321
|
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
328
|
+
// Try to parse content as JSON for structured display
|
|
329
|
+
let contentHtml = '';
|
|
1365
330
|
try {
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1454
|
-
const
|
|
1455
|
-
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1559
|
-
const input = document.getElementById('messageInput');
|
|
379
|
+
setupEventListeners() {
|
|
1560
380
|
const sendBtn = document.getElementById('sendBtn');
|
|
1561
|
-
if (sendBtn)
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1738
|
-
console.
|
|
1739
|
-
|
|
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
|
-
|
|
1796
|
-
|
|
1797
|
-
} else {
|
|
1798
|
-
initializeApp();
|
|
416
|
+
function sendMessage() {
|
|
417
|
+
app.sendMessage();
|
|
1799
418
|
}
|
|
419
|
+
|
|
420
|
+
window.addEventListener('load', initializeApp);
|