agentgui 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/app.js ADDED
@@ -0,0 +1,925 @@
1
+ const BASE_URL = window.__BASE_URL || '';
2
+
3
+ // Auto-reconnecting WebSocket wrapper
4
+ class ReconnectingWebSocket {
5
+ constructor(url, options = {}) {
6
+ this.url = url;
7
+ this.reconnectDelay = options.reconnectDelay || 1000;
8
+ this.maxReconnectDelay = options.maxReconnectDelay || 30000;
9
+ this.reconnectDecay = options.reconnectDecay || 1.5;
10
+ this.currentDelay = this.reconnectDelay;
11
+ this.ws = null;
12
+ this.listeners = new Map();
13
+ this.reconnectAttempts = 0;
14
+ this.maxReconnectAttempts = options.maxReconnectAttempts || Infinity;
15
+ this.shouldReconnect = true;
16
+ this.connect();
17
+ }
18
+
19
+ connect() {
20
+ this.ws = new WebSocket(this.url);
21
+
22
+ this.ws.onopen = (e) => {
23
+ this.currentDelay = this.reconnectDelay;
24
+ this.reconnectAttempts = 0;
25
+ this.emit('open', e);
26
+ };
27
+
28
+ this.ws.onmessage = (e) => {
29
+ this.emit('message', e);
30
+ };
31
+
32
+ this.ws.onerror = (e) => {
33
+ this.emit('error', e);
34
+ };
35
+
36
+ this.ws.onclose = (e) => {
37
+ this.emit('close', e);
38
+ if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
39
+ setTimeout(() => {
40
+ this.reconnectAttempts++;
41
+ this.currentDelay = Math.min(
42
+ this.currentDelay * this.reconnectDecay,
43
+ this.maxReconnectDelay
44
+ );
45
+ console.log(`Attempting to reconnect (${this.reconnectAttempts})...`);
46
+ this.connect();
47
+ }, this.currentDelay);
48
+ }
49
+ };
50
+ }
51
+
52
+ on(event, callback) {
53
+ if (!this.listeners.has(event)) {
54
+ this.listeners.set(event, []);
55
+ }
56
+ this.listeners.get(event).push(callback);
57
+ }
58
+
59
+ emit(event, data) {
60
+ const callbacks = this.listeners.get(event);
61
+ if (callbacks) {
62
+ callbacks.forEach(cb => cb(data));
63
+ }
64
+ }
65
+
66
+ send(data) {
67
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
68
+ this.ws.send(data);
69
+ }
70
+ }
71
+
72
+ close() {
73
+ this.shouldReconnect = false;
74
+ if (this.ws) {
75
+ this.ws.close();
76
+ }
77
+ }
78
+ }
79
+
80
+ class GMGUIApp {
81
+ constructor() {
82
+ this.agents = new Map();
83
+ this.selectedAgent = null;
84
+ this.conversations = new Map();
85
+ this.currentConversation = null;
86
+ this.activeStream = null;
87
+ this.pollingInterval = null;
88
+ this.syncWs = null;
89
+ this.broadcastChannel = null;
90
+ this.settings = { autoScroll: true, connectTimeout: 30000 };
91
+ this.pendingMessages = new Map();
92
+ this.idempotencyKeys = new Map();
93
+ this.init();
94
+ }
95
+
96
+ async init() {
97
+ this.loadSettings();
98
+ this.setupEventListeners();
99
+ await this.fetchHome();
100
+ await this.fetchAgents();
101
+ await this.fetchConversations();
102
+ this.connectSyncWebSocket();
103
+ this.setupCrossTabSync();
104
+ this.renderAll();
105
+ }
106
+
107
+ connectSyncWebSocket() {
108
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
109
+ this.syncWs = new ReconnectingWebSocket(
110
+ `${proto}//${location.host}${BASE_URL}/sync`
111
+ );
112
+
113
+ this.syncWs.on('open', () => {
114
+ console.log('Sync WebSocket connected');
115
+ this.updateConnectionStatus('connected');
116
+ });
117
+
118
+ this.syncWs.on('message', (e) => {
119
+ try {
120
+ const event = JSON.parse(e.data);
121
+ this.handleSyncEvent(event, false);
122
+ } catch (err) {
123
+ console.error('Sync message parse error:', err);
124
+ }
125
+ });
126
+
127
+ this.syncWs.on('close', () => {
128
+ console.log('Sync WebSocket disconnected, will auto-reconnect...');
129
+ this.updateConnectionStatus('reconnecting');
130
+ });
131
+
132
+ this.syncWs.on('error', (err) => {
133
+ console.error('Sync WebSocket error:', err);
134
+ this.updateConnectionStatus('disconnected');
135
+ });
136
+ }
137
+
138
+ setupCrossTabSync() {
139
+ if ('BroadcastChannel' in window) {
140
+ try {
141
+ this.broadcastChannel = new BroadcastChannel('gmgui-sync');
142
+ this.broadcastChannel.onmessage = (e) => {
143
+ this.handleSyncEvent(e.data, true);
144
+ };
145
+ } catch (err) {
146
+ console.error('BroadcastChannel error:', err);
147
+ }
148
+ }
149
+ }
150
+
151
+ handleSyncEvent(event, fromBroadcast = false) {
152
+ switch (event.type) {
153
+ case 'sync_connected':
154
+ break;
155
+
156
+ case 'conversation_created':
157
+ this.conversations.set(event.conversation.id, event.conversation);
158
+ this.renderChatHistory();
159
+ if (!fromBroadcast && this.broadcastChannel) {
160
+ this.broadcastChannel.postMessage(event);
161
+ }
162
+ break;
163
+
164
+ case 'conversation_updated':
165
+ this.conversations.set(event.conversation.id, event.conversation);
166
+ this.renderChatHistory();
167
+ if (this.currentConversation?.id === event.conversation.id) {
168
+ this.currentConversation = event.conversation;
169
+ this.renderCurrentConversation();
170
+ }
171
+ if (!fromBroadcast && this.broadcastChannel) {
172
+ this.broadcastChannel.postMessage(event);
173
+ }
174
+ break;
175
+
176
+ case 'conversation_deleted':
177
+ this.conversations.delete(event.conversationId);
178
+ this.renderChatHistory();
179
+ if (this.currentConversation?.id === event.conversationId) {
180
+ this.currentConversation = null;
181
+ this.renderCurrentConversation();
182
+ }
183
+ if (!fromBroadcast && this.broadcastChannel) {
184
+ this.broadcastChannel.postMessage(event);
185
+ }
186
+ break;
187
+
188
+ case 'message_created':
189
+ if (!fromBroadcast && this.broadcastChannel) {
190
+ this.broadcastChannel.postMessage(event);
191
+ }
192
+ break;
193
+
194
+ case 'session_updated':
195
+ if (event.status === 'completed' && event.message) {
196
+ if (this.currentConversation === event.conversationId) {
197
+ this.addMessageToDisplay(event.message);
198
+ if (this.settings.autoScroll) {
199
+ const div = document.getElementById('chatMessages');
200
+ if (div) div.scrollTop = div.scrollHeight;
201
+ }
202
+ }
203
+ }
204
+ if (!fromBroadcast && this.broadcastChannel) {
205
+ this.broadcastChannel.postMessage(event);
206
+ }
207
+ break;
208
+ }
209
+ }
210
+
211
+ updateConnectionStatus(status) {
212
+ const el = document.getElementById('connectionStatus');
213
+ if (!el) return;
214
+
215
+ el.className = `connection-status ${status}`;
216
+ const text = el.querySelector('.status-text');
217
+ if (text) {
218
+ text.textContent = status === 'connected' ? 'Connected' :
219
+ status === 'reconnecting' ? 'Reconnecting...' :
220
+ 'Disconnected';
221
+ }
222
+ }
223
+
224
+ async fetchHome() {
225
+ try {
226
+ const res = await fetch(BASE_URL + '/api/home');
227
+ if (res.ok) {
228
+ const data = await res.json();
229
+ localStorage.setItem('gmgui-home', data.home);
230
+ }
231
+ } catch (e) {
232
+ console.error('fetchHome:', e);
233
+ }
234
+ }
235
+
236
+ loadSettings() {
237
+ const stored = localStorage.getItem('gmgui-settings');
238
+ if (stored) {
239
+ try { this.settings = { ...this.settings, ...JSON.parse(stored) }; } catch (_) {}
240
+ }
241
+ this.applySettings();
242
+ }
243
+
244
+ saveSettings() {
245
+ localStorage.setItem('gmgui-settings', JSON.stringify(this.settings));
246
+ }
247
+
248
+ applySettings() {
249
+ const el = document.getElementById('autoScroll');
250
+ if (el) el.checked = this.settings.autoScroll;
251
+ const t = document.getElementById('connectTimeout');
252
+ if (t) t.value = this.settings.connectTimeout / 1000;
253
+ }
254
+
255
+ expandHome(p) {
256
+ if (!p) return p;
257
+ const home = localStorage.getItem('gmgui-home') || '/config';
258
+ return p.startsWith('~') ? p.replace('~', home) : p;
259
+ }
260
+
261
+ setupEventListeners() {
262
+ const input = document.getElementById('messageInput');
263
+ if (input) {
264
+ input.addEventListener('keydown', (e) => {
265
+ if (e.key === 'Enter' && !e.shiftKey) {
266
+ e.preventDefault();
267
+ this.sendMessage();
268
+ }
269
+ });
270
+ input.addEventListener('input', () => this.updateSendButtonState());
271
+ }
272
+ document.getElementById('autoScroll')?.addEventListener('change', (e) => {
273
+ this.settings.autoScroll = e.target.checked;
274
+ this.saveSettings();
275
+ });
276
+ document.getElementById('connectTimeout')?.addEventListener('change', (e) => {
277
+ this.settings.connectTimeout = parseInt(e.target.value) * 1000;
278
+ this.saveSettings();
279
+ });
280
+ }
281
+
282
+ async fetchAgents() {
283
+ try {
284
+ const res = await fetch(BASE_URL + '/api/agents');
285
+ const data = await res.json();
286
+ if (data.agents) {
287
+ data.agents.forEach(a => this.agents.set(a.id, a));
288
+ }
289
+ } catch (e) {
290
+ console.error('fetchAgents:', e);
291
+ }
292
+ }
293
+
294
+ async fetchConversations() {
295
+ try {
296
+ const res = await fetch(BASE_URL + '/api/conversations');
297
+ const data = await res.json();
298
+ if (data.conversations) {
299
+ this.conversations.clear();
300
+ data.conversations.forEach(c => this.conversations.set(c.id, c));
301
+ }
302
+ } catch (e) {
303
+ console.error('fetchConversations:', e);
304
+ }
305
+ }
306
+
307
+ async fetchMessages(conversationId) {
308
+ try {
309
+ const res = await fetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
310
+ const data = await res.json();
311
+ return data.messages || [];
312
+ } catch (e) {
313
+ console.error('fetchMessages:', e);
314
+ return [];
315
+ }
316
+ }
317
+
318
+ renderAll() {
319
+ this.renderAgentCards();
320
+ this.renderChatHistory();
321
+ if (this.currentConversation) {
322
+ this.displayConversation(this.currentConversation);
323
+ }
324
+ }
325
+
326
+ renderAgentCards() {
327
+ const container = document.getElementById('agentCards');
328
+ if (!container) return;
329
+ container.innerHTML = '';
330
+ if (this.agents.size === 0) {
331
+ container.innerHTML = '<p style="color: var(--text-tertiary); font-size: 0.875rem;">No agents found. Install claude or opencode.</p>';
332
+ return;
333
+ }
334
+ let first = true;
335
+ this.agents.forEach((agent, id) => {
336
+ if (!first) {
337
+ const sep = document.createElement('span');
338
+ sep.className = 'agent-separator';
339
+ sep.textContent = '|';
340
+ container.appendChild(sep);
341
+ }
342
+ first = false;
343
+ const card = document.createElement('button');
344
+ card.className = `agent-card ${this.selectedAgent === id ? 'active' : ''}`;
345
+ card.onclick = () => this.selectAgent(id);
346
+ card.innerHTML = `
347
+ <span class="agent-card-icon">${escapeHtml(agent.icon || 'A')}</span>
348
+ <span class="agent-card-name">${escapeHtml(agent.name || id)}</span>
349
+ `;
350
+ container.appendChild(card);
351
+ });
352
+ }
353
+
354
+ selectAgent(id) {
355
+ this.selectedAgent = id;
356
+ localStorage.setItem('gmgui-selectedAgent', id);
357
+ this.renderAgentCards();
358
+ const welcome = document.querySelector('.welcome-section');
359
+ if (welcome) welcome.style.display = 'none';
360
+ const input = document.getElementById('messageInput');
361
+ if (input) input.focus();
362
+ }
363
+
364
+ renderChatHistory() {
365
+ const list = document.getElementById('chatList');
366
+ if (!list) return;
367
+ list.innerHTML = '';
368
+ if (this.conversations.size === 0) {
369
+ list.innerHTML = '<p style="color: var(--text-tertiary); font-size: 0.875rem; padding: 0.5rem;">No chats yet</p>';
370
+ return;
371
+ }
372
+ const sorted = Array.from(this.conversations.values()).sort(
373
+ (a, b) => (b.updated_at || 0) - (a.updated_at || 0)
374
+ );
375
+ sorted.forEach(conv => {
376
+ const item = document.createElement('button');
377
+ item.className = `chat-item ${this.currentConversation === conv.id ? 'active' : ''}`;
378
+ const titleSpan = document.createElement('span');
379
+ titleSpan.className = 'chat-item-title';
380
+ titleSpan.textContent = conv.title || 'Untitled';
381
+ const deleteBtn = document.createElement('button');
382
+ deleteBtn.className = 'chat-item-delete';
383
+ deleteBtn.textContent = 'x';
384
+ deleteBtn.title = 'Delete chat';
385
+ deleteBtn.onclick = (e) => {
386
+ e.stopPropagation();
387
+ this.deleteConversation(conv.id);
388
+ };
389
+ item.appendChild(titleSpan);
390
+ item.appendChild(deleteBtn);
391
+ item.onclick = () => this.displayConversation(conv.id);
392
+ list.appendChild(item);
393
+ });
394
+ }
395
+
396
+ async deleteConversation(id) {
397
+ try {
398
+ const res = await fetch(`${BASE_URL}/api/conversations/${id}`, { method: 'DELETE' });
399
+ } catch (e) {
400
+ console.error('deleteConversation:', e);
401
+ }
402
+ this.conversations.delete(id);
403
+ if (this.currentConversation === id) {
404
+ this.currentConversation = null;
405
+ const first = Array.from(this.conversations.values())[0];
406
+ if (first) {
407
+ this.displayConversation(first.id);
408
+ } else {
409
+ this.showWelcome();
410
+ }
411
+ }
412
+ this.renderChatHistory();
413
+ }
414
+
415
+ showWelcome() {
416
+ const div = document.getElementById('chatMessages');
417
+ if (!div) return;
418
+ div.innerHTML = `
419
+ <div class="welcome-section">
420
+ <h2>Hi, what's your plan for today?</h2>
421
+ <div class="agent-selection">
422
+ <div id="agentCards" class="agent-cards"></div>
423
+ </div>
424
+ </div>
425
+ `;
426
+ this.renderAgentCards();
427
+ }
428
+
429
+ async displayConversation(id) {
430
+ this.currentConversation = id;
431
+ const conv = this.conversations.get(id);
432
+ if (!conv) return;
433
+ if (conv.agentId && !this.selectedAgent) {
434
+ this.selectedAgent = conv.agentId;
435
+ }
436
+
437
+ const messages = await this.fetchMessages(id);
438
+
439
+ const div = document.getElementById('chatMessages');
440
+ if (!div) return;
441
+ div.innerHTML = '';
442
+
443
+ if (messages.length === 0 && !this.selectedAgent) {
444
+ div.innerHTML = `
445
+ <div class="welcome-section">
446
+ <h2>Hi, what's your plan for today?</h2>
447
+ <div class="agent-selection">
448
+ <div id="agentCards" class="agent-cards"></div>
449
+ </div>
450
+ </div>
451
+ `;
452
+ this.renderAgentCards();
453
+ } else {
454
+ messages.forEach(msg => this.addMessageToDisplay(msg));
455
+
456
+ if (this.settings.autoScroll) {
457
+ div.scrollTop = div.scrollHeight;
458
+ }
459
+ }
460
+ this.renderChatHistory();
461
+ this.renderAgentCards();
462
+ }
463
+
464
+
465
+ parseAndRenderContent(content) {
466
+ const elements = [];
467
+ if (typeof content === 'string') {
468
+ const htmlCodeBlockRegex = /```html\n([\s\S]*?)\n```/g;
469
+ let lastIndex = 0;
470
+ let match;
471
+
472
+ while ((match = htmlCodeBlockRegex.exec(content)) !== null) {
473
+ if (match.index > lastIndex) {
474
+ const textBefore = content.substring(lastIndex, match.index);
475
+ if (textBefore.trim()) {
476
+ const bubble = document.createElement('div');
477
+ bubble.className = 'message-bubble';
478
+ bubble.textContent = textBefore;
479
+ elements.push(bubble);
480
+ }
481
+ }
482
+
483
+ const htmlContent = match[1];
484
+ const htmlEl = this.createHtmlBlock({ html: htmlContent });
485
+ elements.push(htmlEl);
486
+ lastIndex = htmlCodeBlockRegex.lastIndex;
487
+ }
488
+
489
+ if (lastIndex < content.length) {
490
+ const textAfter = content.substring(lastIndex);
491
+ if (textAfter.trim()) {
492
+ const bubble = document.createElement('div');
493
+ bubble.className = 'message-bubble';
494
+ bubble.textContent = textAfter;
495
+ elements.push(bubble);
496
+ }
497
+ }
498
+
499
+ return elements.length > 0 ? elements : null;
500
+ }
501
+ return null;
502
+ }
503
+
504
+ addMessageToDisplay(msg) {
505
+ const div = document.getElementById('chatMessages');
506
+ if (!div) return;
507
+ const el = document.createElement('div');
508
+ el.className = `message ${msg.role}`;
509
+ el.dataset.messageId = msg.id;
510
+
511
+ if (typeof msg.content === 'string') {
512
+ const parsed = this.parseAndRenderContent(msg.content);
513
+ if (parsed) {
514
+ parsed.forEach(elem => el.appendChild(elem));
515
+ } else {
516
+ const bubble = document.createElement('div');
517
+ bubble.className = 'message-bubble';
518
+ bubble.textContent = msg.content;
519
+ el.appendChild(bubble);
520
+ }
521
+ } else if (typeof msg.content === 'object' && msg.content !== null) {
522
+ if (msg.content.text) {
523
+ const parsed = this.parseAndRenderContent(msg.content.text);
524
+ if (parsed) {
525
+ parsed.forEach(elem => el.appendChild(elem));
526
+ } else {
527
+ const bubble = document.createElement('div');
528
+ bubble.className = 'message-bubble';
529
+ bubble.textContent = msg.content.text;
530
+ el.appendChild(bubble);
531
+ }
532
+ }
533
+ if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
534
+ msg.content.blocks.forEach(block => {
535
+ if (block.type === 'html') {
536
+ const htmlEl = this.createHtmlBlock(block);
537
+ el.appendChild(htmlEl);
538
+ } else if (block.type === 'image') {
539
+ const imgEl = this.createImageBlock(block);
540
+ el.appendChild(imgEl);
541
+ }
542
+ });
543
+ }
544
+ } else {
545
+ const bubble = document.createElement('div');
546
+ bubble.className = 'message-bubble';
547
+ bubble.textContent = JSON.stringify(msg.content);
548
+ el.appendChild(bubble);
549
+ }
550
+
551
+ div.appendChild(el);
552
+ }
553
+
554
+ async startNewChat(folderPath) {
555
+ if (!this.selectedAgent) {
556
+ const firstAgent = Array.from(this.agents.keys())[0];
557
+ if (firstAgent) {
558
+ this.selectedAgent = firstAgent;
559
+ }
560
+ }
561
+ const title = folderPath
562
+ ? folderPath.split('/').pop() || folderPath
563
+ : `Chat ${this.conversations.size + 1}`;
564
+ try {
565
+ const res = await fetch(BASE_URL + '/api/conversations', {
566
+ method: 'POST',
567
+ headers: { 'Content-Type': 'application/json' },
568
+ body: JSON.stringify({ agentId: this.selectedAgent || 'claude-code', title }),
569
+ });
570
+ const data = await res.json();
571
+ if (data.conversation) {
572
+ const conv = data.conversation;
573
+ if (folderPath) conv.folderPath = folderPath;
574
+ this.conversations.set(conv.id, conv);
575
+ this.currentConversation = conv.id;
576
+ this.renderChatHistory();
577
+ this.displayConversation(conv.id);
578
+ }
579
+ } catch (e) {
580
+ console.error('startNewChat:', e);
581
+ }
582
+ }
583
+
584
+ async sendMessage() {
585
+ const input = document.getElementById('messageInput');
586
+ const message = input.value.trim();
587
+ if (!message) return;
588
+ if (!this.selectedAgent) {
589
+ this.addSystemMessage('Please select an agent first');
590
+ return;
591
+ }
592
+ if (!this.currentConversation) {
593
+ await this.startNewChat();
594
+ }
595
+ if (!this.currentConversation) return;
596
+ const conv = this.conversations.get(this.currentConversation);
597
+
598
+ const idempotencyKey = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
599
+ this.addMessageToDisplay({ role: 'user', content: message });
600
+ input.value = '';
601
+ this.updateSendButtonState();
602
+
603
+ try {
604
+ const folderPath = conv?.folderPath || localStorage.getItem('gmgui-home') || '/config';
605
+ const res = await fetch(`${BASE_URL}/api/conversations/${this.currentConversation}/messages`, {
606
+ method: 'POST',
607
+ headers: { 'Content-Type': 'application/json' },
608
+ body: JSON.stringify({
609
+ content: message,
610
+ agentId: this.selectedAgent,
611
+ folderContext: { path: folderPath, isFolder: true },
612
+ idempotencyKey,
613
+ }),
614
+ });
615
+ if (!res.ok) {
616
+ const err = await res.json();
617
+ this.addMessageToDisplay({ role: 'system', content: `Error: ${err.error || 'Request failed'}` });
618
+ return;
619
+ }
620
+ const data = await res.json();
621
+ this.idempotencyKeys.set(idempotencyKey, data.session.id);
622
+ this.startPollingMessages(this.currentConversation);
623
+ } catch (e) {
624
+ this.addMessageToDisplay({ role: 'system', content: `Error: ${e.message}` });
625
+ }
626
+ if (this.settings.autoScroll) {
627
+ const div = document.getElementById('chatMessages');
628
+ if (div) div.scrollTop = div.scrollHeight;
629
+ }
630
+ }
631
+
632
+ addSystemMessage(text) {
633
+ this.addMessageToDisplay({ role: 'system', content: text });
634
+ }
635
+
636
+ startPollingMessages(conversationId) {
637
+ if (this.pollingInterval) clearInterval(this.pollingInterval);
638
+
639
+ let pollCount = 0;
640
+ const maxNoResponsePolls = 60; // Stop polling after 60 polls with no change
641
+ let lastMessageCount = 0;
642
+
643
+ this.pollingInterval = setInterval(async () => {
644
+ try {
645
+ const res = await fetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
646
+ const data = await res.json();
647
+ const messages = data.messages || [];
648
+
649
+ // If we got new messages, render them
650
+ if (messages.length > lastMessageCount) {
651
+ const newMessages = messages.slice(lastMessageCount);
652
+ newMessages.forEach(msg => {
653
+ const existingEl = document.querySelector(`[data-message-id="${msg.id}"]`);
654
+ if (!existingEl) {
655
+ this.addMessageToDisplay(msg);
656
+ }
657
+ });
658
+ lastMessageCount = messages.length;
659
+ pollCount = 0; // Reset counter when we get activity
660
+
661
+ if (this.settings.autoScroll) {
662
+ const div = document.getElementById('chatMessages');
663
+ if (div) div.scrollTop = div.scrollHeight;
664
+ }
665
+ } else {
666
+ pollCount++;
667
+ }
668
+
669
+ // Stop polling if no changes for a while
670
+ if (pollCount > maxNoResponsePolls) {
671
+ clearInterval(this.pollingInterval);
672
+ this.pollingInterval = null;
673
+ }
674
+ } catch (e) {
675
+ console.error('Polling error:', e);
676
+ clearInterval(this.pollingInterval);
677
+ this.pollingInterval = null;
678
+ }
679
+ }, 500); // Poll every 500ms
680
+ }
681
+
682
+ createThoughtBlock() {
683
+ const wrap = document.createElement('div');
684
+ wrap.className = 'thought-block';
685
+ const header = document.createElement('div');
686
+ header.className = 'thought-header';
687
+ header.textContent = 'Thinking...';
688
+ header.onclick = () => wrap.classList.toggle('collapsed');
689
+ const content = document.createElement('div');
690
+ content.className = 'thought-content';
691
+ wrap.appendChild(header);
692
+ wrap.appendChild(content);
693
+ return wrap;
694
+ }
695
+
696
+ createToolBlock(event) {
697
+ const wrap = document.createElement('div');
698
+ wrap.className = `tool-block status-${event.status || 'running'}`;
699
+ const header = document.createElement('div');
700
+ header.className = 'tool-header';
701
+ const kindIcons = { execute: '>', read: '?', edit: '/', search: '~', fetch: '@', write: '/', think: '!', other: '#' };
702
+ const icon = kindIcons[event.kind] || '#';
703
+ 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>`;
704
+ header.onclick = () => wrap.classList.toggle('collapsed');
705
+ wrap.appendChild(header);
706
+ if (event.content && event.content.length) {
707
+ const body = document.createElement('div');
708
+ body.className = 'tool-body';
709
+ event.content.forEach(c => {
710
+ if (c.text) body.textContent += c.text;
711
+ });
712
+ wrap.appendChild(body);
713
+ }
714
+ return wrap;
715
+ }
716
+
717
+ updateToolBlock(block, event) {
718
+ block.className = `tool-block status-${event.status || 'completed'}`;
719
+ const statusEl = block.querySelector('.tool-status');
720
+ if (statusEl) statusEl.textContent = event.status || 'completed';
721
+ if (event.content && event.content.length) {
722
+ let body = block.querySelector('.tool-body');
723
+ if (!body) { body = document.createElement('div'); body.className = 'tool-body'; block.appendChild(body); }
724
+ event.content.forEach(c => {
725
+ if (c.text) body.textContent += c.text;
726
+ });
727
+ }
728
+ }
729
+
730
+ createPlanBlock(entries) {
731
+ const wrap = document.createElement('div');
732
+ wrap.className = 'plan-block';
733
+ const header = document.createElement('div');
734
+ header.className = 'plan-header';
735
+ header.textContent = 'Plan';
736
+ wrap.appendChild(header);
737
+ if (entries && entries.length) {
738
+ entries.forEach(entry => {
739
+ const item = document.createElement('div');
740
+ item.className = 'plan-item';
741
+ item.textContent = entry.title || entry.description || JSON.stringify(entry);
742
+ wrap.appendChild(item);
743
+ });
744
+ }
745
+ return wrap;
746
+ }
747
+
748
+ createHtmlBlock(event) {
749
+ const wrap = document.createElement('div');
750
+ wrap.className = 'html-block';
751
+ if (event.id) wrap.id = `html-${event.id}`;
752
+ if (event.title) {
753
+ const header = document.createElement('div');
754
+ header.className = 'html-header';
755
+ header.textContent = event.title;
756
+ wrap.appendChild(header);
757
+ }
758
+ const content = document.createElement('div');
759
+ content.className = 'html-content';
760
+ content.innerHTML = event.html;
761
+ wrap.appendChild(content);
762
+ return wrap;
763
+ }
764
+
765
+ createImageBlock(event) {
766
+ const wrap = document.createElement('div');
767
+ wrap.className = 'image-block';
768
+ if (event.title) {
769
+ const header = document.createElement('div');
770
+ header.className = 'image-header';
771
+ header.textContent = event.title;
772
+ wrap.appendChild(header);
773
+ }
774
+ const img = document.createElement('img');
775
+ img.src = event.url;
776
+ img.alt = event.alt || 'Image from agent';
777
+ img.className = 'image-content';
778
+ img.style.maxWidth = '100%';
779
+ img.style.height = 'auto';
780
+ img.style.borderRadius = '0.25rem';
781
+ wrap.appendChild(img);
782
+ return wrap;
783
+ }
784
+
785
+ updateSendButtonState() {
786
+ const input = document.getElementById('messageInput');
787
+ const btn = document.getElementById('sendBtn');
788
+ if (btn) btn.disabled = !input || !input.value.trim();
789
+ }
790
+
791
+ openFolderBrowser() {
792
+ const modal = document.getElementById('folderBrowserModal');
793
+ if (!modal) return;
794
+ const pathInput = document.getElementById('folderPath');
795
+ pathInput.value = '~/';
796
+ this.loadFolderContents(this.expandHome('~/'));
797
+ modal.classList.add('active');
798
+ }
799
+
800
+ closeFolderBrowser() {
801
+ const modal = document.getElementById('folderBrowserModal');
802
+ if (modal) modal.classList.remove('active');
803
+ }
804
+
805
+ async loadFolderContents(folderPath) {
806
+ const list = document.getElementById('folderBrowserList');
807
+ if (!list) return;
808
+ list.innerHTML = '<div style="padding: 1rem; color: var(--text-tertiary);">Loading...</div>';
809
+ try {
810
+ const res = await fetch(BASE_URL + '/api/folders', {
811
+ method: 'POST',
812
+ headers: { 'Content-Type': 'application/json' },
813
+ body: JSON.stringify({ path: folderPath }),
814
+ });
815
+ if (res.ok) {
816
+ const data = await res.json();
817
+ this.renderFolderList(data.folders, folderPath);
818
+ } else {
819
+ list.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">Error loading folder</div>';
820
+ }
821
+ } catch (e) {
822
+ list.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">Error: ' + e.message + '</div>';
823
+ }
824
+ }
825
+
826
+ renderFolderList(folders, currentPath) {
827
+ const list = document.getElementById('folderBrowserList');
828
+ if (!list) return;
829
+ list.innerHTML = '';
830
+ if (currentPath !== '/' && currentPath !== '/root') {
831
+ const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
832
+ const parentItem = document.createElement('div');
833
+ parentItem.className = 'folder-item';
834
+ parentItem.style.cssText = 'padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid var(--border-color);';
835
+ parentItem.innerHTML = '<span>../</span>';
836
+ parentItem.onclick = () => {
837
+ document.getElementById('folderPath').value = parentPath;
838
+ this.loadFolderContents(parentPath);
839
+ };
840
+ list.appendChild(parentItem);
841
+ }
842
+ if (!folders || folders.length === 0) {
843
+ const empty = document.createElement('div');
844
+ empty.style.cssText = 'padding: 1rem; color: var(--text-tertiary); text-align: center;';
845
+ empty.textContent = 'No subfolders found';
846
+ list.appendChild(empty);
847
+ return;
848
+ }
849
+ folders.forEach(folder => {
850
+ const item = document.createElement('div');
851
+ item.style.cssText = 'padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid var(--border-color);';
852
+ item.textContent = folder.name;
853
+ item.onclick = () => {
854
+ const newPath = currentPath === '/' ? '/' + folder.name : currentPath + '/' + folder.name;
855
+ document.getElementById('folderPath').value = newPath;
856
+ this.loadFolderContents(newPath);
857
+ };
858
+ list.appendChild(item);
859
+ });
860
+ }
861
+ }
862
+
863
+ function escapeHtml(text) {
864
+ const div = document.createElement('div');
865
+ div.textContent = text;
866
+ return div.innerHTML;
867
+ }
868
+
869
+ function showNewChatModal() {
870
+ const modal = document.getElementById('newChatModal');
871
+ if (modal) modal.classList.add('active');
872
+ }
873
+
874
+ function closeNewChatModal() {
875
+ const modal = document.getElementById('newChatModal');
876
+ if (modal) modal.classList.remove('active');
877
+ }
878
+
879
+ function createChatInWorkspace() {
880
+ closeNewChatModal();
881
+ app.startNewChat();
882
+ }
883
+
884
+ function createChatInFolder() {
885
+ closeNewChatModal();
886
+ app.openFolderBrowser();
887
+ }
888
+
889
+ function sendMessage() { app.sendMessage(); }
890
+
891
+ function toggleSidebar() {
892
+ const sidebar = document.getElementById('sidebar');
893
+ if (sidebar) sidebar.classList.toggle('open');
894
+ }
895
+
896
+ function switchTab(tabName) {
897
+ const panel = document.getElementById('settingsPanel');
898
+ const main = document.querySelector('.main-content');
899
+ if (tabName === 'settings' && panel && main) {
900
+ panel.style.display = 'flex';
901
+ main.style.display = 'none';
902
+ } else if (tabName === 'chat' && panel && main) {
903
+ panel.style.display = 'none';
904
+ main.style.display = 'flex';
905
+ }
906
+ }
907
+
908
+ function closeFolderBrowser() { app.closeFolderBrowser(); }
909
+
910
+ function browseFolders() {
911
+ const pathInput = document.getElementById('folderPath');
912
+ const p = pathInput.value.trim() || '~/';
913
+ app.loadFolderContents(app.expandHome(p));
914
+ }
915
+
916
+ function confirmFolderSelection() {
917
+ const pathInput = document.getElementById('folderPath');
918
+ const p = pathInput.value.trim();
919
+ if (!p) return;
920
+ app.startNewChat(app.expandHome(p));
921
+ app.closeFolderBrowser();
922
+ }
923
+
924
+ const app = new GMGUIApp();
925
+ window._app = app;