agentgui 1.0.829 → 1.0.831

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.
@@ -1,3211 +1,145 @@
1
-
2
- class AgentGUIClient {
3
- constructor(config = {}) {
4
- this.config = {
5
- containerId: config.containerId || 'app',
6
- outputContainerId: config.outputContainerId || 'output',
7
- scrollContainerId: config.scrollContainerId || 'output-scroll',
8
- autoConnect: config.autoConnect !== false,
9
- ...config
10
- };
11
-
12
- this.renderer = new StreamingRenderer(config.renderer || {});
13
- this.wsManager = window.wsManager || new WebSocketManager(config.websocket || {});
14
- if (!window.wsManager) window.wsManager = this.wsManager;
15
- this.eventProcessor = new EventProcessor(config.eventProcessor || {});
16
-
17
- this.state = {
18
- isInitialized: false,
19
- currentSession: null,
20
- currentConversation: null,
21
- streamingConversations: new Map(),
22
- sessionEvents: [],
23
- conversations: [],
24
- agents: []
25
- };
26
-
27
- this.conversationCache = new Map();
28
- this.MAX_CACHE_SIZE = 10;
29
-
30
- this.conversationListCache = {
31
- data: [],
32
- timestamp: 0,
33
- ttl: 30000 // 30 seconds
34
- };
35
-
36
- this.draftPrompts = new Map();
37
-
38
- this.eventHandlers = {};
39
-
40
- this.ui = {
41
- statusIndicator: null,
42
- messageInput: null,
43
- sendButton: null,
44
- cliSelector: null,
45
- agentSelector: null,
46
- modelSelector: null
47
- };
48
-
49
- this._agentLocked = false;
50
- this._isLoadingConversation = false;
51
- this._modelCache = new Map();
52
-
53
- this._renderedSeqs = {}; // plain object: sessionId → Set<number>
54
- this._inflightRequests = new Map();
55
- this._previousConvAbort = null;
56
-
57
- this._bgCache = new Map();
58
- this.BG_CACHE_MAX = 50;
59
-
60
- this._loadInProgress = {}; // { [conversationId]: { requestId, abortController, timestamp, prevConversationId } }
61
- this._currentRequestId = 0; // Auto-incrementing request counter
62
-
63
-
64
- this._scrollTarget = 0;
65
- this._scrollAnimating = false;
66
- this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
67
-
68
-
69
- this._serverProcessingEstimate = 2000;
70
- this._lastSendTime = 0;
71
- this._countdownTimer = null;
72
-
73
- this.routerState = {
74
- currentConversationId: null,
75
- currentSessionId: null
76
- };
77
-
78
- this._debug = typeof localStorage !== 'undefined' && localStorage.getItem('debug') === '1';
79
- }
80
-
81
- _dbg(...args) { if (this._debug) console.log('[AgentGUI]', ...args); }
82
-
83
- async init() {
84
- try {
85
- this._dbg('Initializing AgentGUI client');
86
-
87
- const wsReady = this.config.autoConnect ? this.connectWebSocket() : Promise.resolve();
88
-
89
- this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
90
-
91
- if (typeof ImageLoader !== 'undefined') {
92
- window.imageLoader = new ImageLoader();
93
- }
94
-
95
- this.setupWebSocketListeners();
96
- this.setupRendererListeners();
97
- this.setupUI();
98
-
99
- await wsReady;
100
- await Promise.all([
101
- this.loadAgents(),
102
- this.loadConversations(),
103
- this.checkSpeechStatus()
104
- ]);
105
-
106
- this.enableControls();
107
-
108
- this.restoreStateFromUrl();
109
-
110
- this.state.isInitialized = true;
111
- this.emit('initialized');
112
- this._setupDebugHooks();
113
-
114
- this._dbg('AgentGUI client initialized');
115
- return this;
116
- } catch (error) {
117
- console.error('Client initialization error:', error);
118
- this.showError('Failed to initialize client: ' + error.message);
119
- throw error;
120
- }
121
- }
122
-
123
- setupWebSocketListeners() {
124
- this.wsManager.on('connected', () => {
125
- this._dbg('WebSocket connected');
126
- this.updateConnectionStatus('connected');
127
- this._subscribeToConversationUpdates();
128
- if (this.wsManager.stats.totalReconnects > 0 && this.state.currentConversation?.id) {
129
- this.invalidateCache(this.state.currentConversation.id);
130
- }
131
- this._recoverMissedChunks();
132
- this.updateSendButtonState();
133
- this.enablePromptArea();
134
- if (this.state.currentConversation?.id) {
135
- this.updateBusyPromptArea(this.state.currentConversation.id);
136
- }
137
- this.emit('ws:connected');
138
- if (window.__SERVER_VERSION) {
139
- fetch((window.__BASE_URL || '') + '/api/version').then(r => r.json()).then(d => {
140
- if (d.version && d.version !== window.__SERVER_VERSION) {
141
- this._dbg(`Server updated ${window.__SERVER_VERSION} → ${d.version}, reloading`);
142
- window.location.reload();
143
- }
144
- }).catch(() => {});
145
- }
146
- });
147
-
148
- this.wsManager.on('disconnected', () => {
149
- this._dbg('WebSocket disconnected');
150
- this.updateConnectionStatus('disconnected');
151
- this.updateSendButtonState();
152
- this.disablePromptArea();
153
- this.emit('ws:disconnected');
154
- });
155
-
156
- this.wsManager.on('reconnecting', (data) => {
157
- this._dbg('WebSocket reconnecting:', data);
158
- this.updateConnectionStatus('reconnecting');
159
- });
160
-
161
- this.wsManager.on('message', (data) => {
162
- this.handleWebSocketMessage(data);
163
- });
164
-
165
- this.wsManager.on('error', (data) => {
166
- console.error('WebSocket error:', data);
167
- });
168
-
169
- this.wsManager.on('latency_update', (data) => {
170
- this._updateConnectionIndicator(data.quality);
171
- });
172
-
173
- this.wsManager.on('connection_degrading', () => {
174
- const dot = document.querySelector('.connection-dot');
175
- if (dot) dot.classList.add('degrading');
176
- });
177
-
178
- this.wsManager.on('connection_recovering', () => {
179
- const dot = document.querySelector('.connection-dot');
180
- if (dot) dot.classList.remove('degrading');
181
- });
182
-
183
- window.addEventListener('conversation-selected', (e) => {
184
- const convId = e.detail.conversationId;
185
- this.saveDraftPrompt();
186
-
187
- const isStreaming = this._convIsStreaming(convId);
188
- if (!isStreaming && window.switchView) {
189
- window.switchView('chat');
190
- }
191
-
192
- this.restoreDraftPrompt(convId);
193
- });
194
-
195
- window.addEventListener('view-switched', (e) => {
196
- const view = e.detail.view;
197
- if (view === 'chat') {
198
- const convId = this.state.currentConversation?.id;
199
- const isStreaming = this._convIsStreaming(convId);
200
- if (isStreaming) {
201
- this.disableControls();
202
- } else {
203
- this.enableControls();
204
- }
205
- }
206
- });
207
- }
208
-
209
- _convIsStreaming(convId) {
210
- if (!convId) return false;
211
- if (typeof convMachineAPI !== 'undefined') return convMachineAPI.isStreaming(convId);
212
- return this.state.streamingConversations.has(convId);
213
- }
214
-
215
- _setConvStreaming(convId, streaming, sessionId, agentId) {
216
- if (!convId) return;
217
- if (streaming) {
218
- this.state.streamingConversations.set(convId, true);
219
- if (typeof convMachineAPI !== 'undefined') convMachineAPI.send(convId, { type: 'STREAM_START', sessionId, agentId });
220
- } else {
221
- this.state.streamingConversations.delete(convId);
222
- if (typeof convMachineAPI !== 'undefined') convMachineAPI.send(convId, { type: 'COMPLETE' });
223
- }
224
- }
225
-
226
- setupRendererListeners() {
227
- this.renderer.on('batch:complete', (data) => {
228
- this._dbg('Batch rendered:', data);
229
- this.updateMetrics(data.metrics);
230
- });
231
-
232
- this.renderer.on('error:render', (data) => {
233
- console.error('Render error:', data.error);
234
- });
235
- }
236
-
237
- restoreStateFromUrl() {
238
- const pathMatch = window.location.pathname.match(/\/conversations\/([^\/]+)$/);
239
- const conversationId = pathMatch ? pathMatch[1] : null;
240
-
241
- const params = new URLSearchParams(window.location.search);
242
- const sessionId = params.get('session');
243
-
244
- if (conversationId && this.isValidId(conversationId)) {
245
- this.routerState.currentConversationId = conversationId;
246
- if (sessionId && this.isValidId(sessionId)) {
247
- this.routerState.currentSessionId = sessionId;
248
- }
249
- this._dbg('Restoring conversation from URL:', conversationId);
250
- this._isLoadingConversation = true;
251
- if (window.conversationManager) {
252
- window.conversationManager.select(conversationId);
253
- } else {
254
- this.loadConversationMessages(conversationId).catch((err) => {
255
- console.warn('Failed to restore conversation from URL, loading latest instead:', err);
256
- if (this.state.conversations && this.state.conversations.length > 0) {
257
- const latestConv = this.state.conversations[0];
258
- this._dbg('Loading latest conversation instead:', latestConv.id);
259
- return this.loadConversationMessages(latestConv.id);
260
- } else {
261
- this._showWelcomeScreen();
262
- }
263
- }).finally(() => {
264
- this._isLoadingConversation = false;
265
- });
266
- }
267
- } else {
268
- this._showWelcomeScreen();
269
- }
270
- }
271
-
272
- isValidId(id) {
273
- if (!id || typeof id !== 'string') return false;
274
- return /^[a-zA-Z0-9_-]+$/.test(id) && id.length < 256;
275
- }
276
-
277
- updateUrlForConversation(conversationId, sessionId) {
278
- if (!this.isValidId(conversationId)) return;
279
- if (!this.routerState) return;
280
-
281
- this.routerState.currentConversationId = conversationId;
282
- if (sessionId && this.isValidId(sessionId)) {
283
- this.routerState.currentSessionId = sessionId;
284
- }
285
-
286
- const basePath = window.location.pathname.replace(/\/conversations\/[^\/]+$/, '').replace(/\/$/, '');
287
- let url = `${basePath}/conversations/${conversationId}`;
288
-
289
- if (sessionId && this.isValidId(sessionId)) {
290
- url += `?session=${sessionId}`;
291
- }
292
-
293
- window.history.pushState({ conversationId, sessionId }, '', url);
294
- }
295
-
296
- saveScrollPosition(conversationId) {
297
- if (!this.isValidId(conversationId)) return;
298
-
299
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
300
- if (scrollContainer) {
301
- const position = scrollContainer.scrollTop;
302
- try {
303
- localStorage.setItem(`scroll_${conversationId}`, position.toString());
304
- } catch (e) {
305
- console.warn('Failed to save scroll position:', e);
306
- }
307
- }
308
- }
309
-
310
- restoreScrollPosition(conversationId) {
311
- if (!this.isValidId(conversationId)) return;
312
-
313
- try {
314
- const position = localStorage.getItem(`scroll_${conversationId}`);
315
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
316
- if (!scrollContainer) return;
317
-
318
- if (position !== null) {
319
- const scrollTop = parseInt(position, 10);
320
- if (!isNaN(scrollTop)) {
321
- requestAnimationFrame(() => {
322
- requestAnimationFrame(() => {
323
- const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
324
- scrollContainer.scrollTop = Math.min(scrollTop, maxScroll);
325
- });
326
- });
327
- }
328
- } else {
329
- requestAnimationFrame(() => {
330
- requestAnimationFrame(() => {
331
- scrollContainer.scrollTop = 0;
332
- });
333
- });
334
- }
335
- } catch (e) {
336
- console.warn('Failed to restore scroll position:', e);
337
- }
338
- }
339
-
340
- setupScrollTracking() {
341
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
342
- if (!scrollContainer) return;
343
-
344
- this._userScrolledUp = false;
345
- let scrollTimer = null;
346
- let lastScrollTop = scrollContainer.scrollTop;
347
- scrollContainer.addEventListener('scroll', () => {
348
- const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
349
- if (scrollContainer.scrollTop < lastScrollTop && distFromBottom > 200) {
350
- this._userScrolledUp = true;
351
- } else if (distFromBottom < 50) {
352
- this._userScrolledUp = false;
353
- this._removeNewContentPill();
354
- }
355
- lastScrollTop = scrollContainer.scrollTop;
356
- if (scrollTimer) clearTimeout(scrollTimer);
357
- scrollTimer = setTimeout(() => {
358
- if (this.state.currentConversation?.id) {
359
- this.saveScrollPosition(this.state.currentConversation.id);
360
- }
361
- }, 500);
362
- });
363
- }
364
-
365
- setupUI() {
366
- const container = document.getElementById(this.config.containerId);
367
- if (!container) {
368
- throw new Error(`Container not found: ${this.config.containerId}`);
369
- }
370
-
371
- this.ui.statusIndicator = document.querySelector('[data-status-indicator]');
372
- this.ui.messageInput = document.querySelector('[data-message-input]');
373
- this.ui.sendButton = document.querySelector('[data-send-button]');
374
- this.ui.cliSelector = document.querySelector('[data-cli-selector]');
375
- this.ui.agentSelector = document.querySelector('[data-agent-selector]');
376
- this.ui.modelSelector = document.querySelector('[data-model-selector]');
377
-
378
- if (this.ui.messageInput) {
379
- this.ui.messageInput.addEventListener('input', () => {
380
- this.saveDraftPrompt();
381
- });
382
-
383
- const currentConvId = this.state.currentConversation?.id;
384
- if (currentConvId) {
385
- this.restoreDraftPrompt(currentConvId);
386
- }
387
- }
388
-
389
- if (this.ui.cliSelector) {
390
- this.ui.cliSelector.addEventListener('change', () => {
391
- if (!this._agentLocked) {
392
- this.loadSubAgentsForCli(this.ui.cliSelector.value);
393
- this.loadModelsForAgent(this.ui.cliSelector.value);
394
- this.saveAgentAndModelToConversation();
395
- }
396
- });
397
- }
398
-
399
- if (this.ui.agentSelector) {
400
- this.ui.agentSelector.addEventListener('change', () => {
401
- const parentAgentId = this.ui.cliSelector?.value;
402
- if (parentAgentId) {
403
- this.loadModelsForAgent(parentAgentId);
404
- }
405
- if (!this._agentLocked) {
406
- this.saveAgentAndModelToConversation();
407
- }
408
- });
409
- }
410
-
411
- if (this.ui.modelSelector) {
412
- this.ui.modelSelector.addEventListener('change', () => {
413
- this.saveAgentAndModelToConversation();
414
- });
415
- }
416
-
417
- if (this.ui.sendButton) {
418
- this.ui.sendButton.addEventListener('click', () => this.startExecution());
419
- }
420
-
421
- this.setupChatMicButton();
422
-
423
- this.ui.stopButton = document.getElementById('stopBtn');
424
- this.ui.injectButton = document.getElementById('injectBtn');
425
- this.ui.queueButton = document.getElementById('queueBtn');
426
-
427
- if (this.ui.stopButton) {
428
- this.ui.stopButton.addEventListener('click', async () => {
429
- if (!this.state.currentConversation) return;
430
- try {
431
- const data = await window.wsClient.rpc('conv.cancel', { id: this.state.currentConversation.id });
432
- this._dbg('Stop response:', data);
433
- } catch (err) {
434
- console.error('Failed to stop:', err);
435
- }
436
- });
437
- }
438
-
439
- if (this.ui.injectButton) {
440
- this.ui.injectButton.addEventListener('click', async () => {
441
- if (!this.state.currentConversation) return;
442
- const isStreaming = this._convIsStreaming(this.state.currentConversation.id);
443
-
444
- if (isStreaming) {
445
- const message = this.ui.messageInput?.value || '';
446
- if (!message.trim()) {
447
- this.showError('Please enter a message to steer');
448
- return;
449
- }
450
-
451
- const steerMsg = message;
452
- if (this.ui.messageInput) {
453
- this.ui.messageInput.value = '';
454
- this.ui.messageInput.style.height = 'auto';
455
- }
456
-
457
- window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: steerMsg })
458
- .catch(err => {
459
- console.error('Failed to steer:', err);
460
- this.showError('Failed to steer: ' + err.message);
461
- });
462
- } else {
463
- const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
464
- if (!instructions) return;
465
- window.wsClient.rpc('conv.inject', { id: this.state.currentConversation.id, content: instructions })
466
- .catch(err => console.error('Failed to inject:', err));
467
- }
468
- });
469
- }
470
-
471
- if (this.ui.queueButton) {
472
- this.ui.queueButton.addEventListener('click', async () => {
473
- if (!this.state.currentConversation) return;
474
- const message = this.ui.messageInput?.value || '';
475
- if (!message.trim()) {
476
- this.showError('Please enter a message to queue');
477
- return;
478
- }
479
- try {
480
- const data = await window.wsClient.rpc('msg.send', { id: this.state.currentConversation.id, content: message });
481
- this._dbg('Queue response:', data);
482
- if (this.ui.messageInput) {
483
- this.ui.messageInput.value = '';
484
- this.ui.messageInput.style.height = 'auto';
485
- }
486
- } catch (err) {
487
- console.error('Failed to queue:', err);
488
- this.showError('Failed to queue: ' + err.message);
489
- }
490
- });
491
- }
492
-
493
- if (this.ui.messageInput) {
494
- this.ui.messageInput.addEventListener('keydown', (e) => {
495
- if (e.key === 'Enter' && e.ctrlKey) {
496
- this.startExecution();
497
- }
498
- });
499
-
500
- this.ui.messageInput.addEventListener('input', () => {
501
- const el = this.ui.messageInput;
502
- el.style.height = 'auto';
503
- el.style.height = Math.min(el.scrollHeight, 150) + 'px';
504
- });
505
- }
506
-
507
- document.addEventListener('keydown', (e) => {
508
- if (e.key === 'n' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
509
- e.preventDefault();
510
- const newBtn = document.querySelector('[data-new-conversation], #newConversationBtn, .new-conversation-btn');
511
- if (newBtn) newBtn.click();
512
- }
513
- if (e.key === 'b' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
514
- e.preventDefault();
515
- const toggleBtn = document.querySelector('[data-sidebar-toggle]');
516
- if (toggleBtn) toggleBtn.click();
517
- }
518
- if (e.key === 'Escape') {
519
- const activeEl = document.activeElement;
520
- if (activeEl && activeEl.tagName === 'TEXTAREA') { activeEl.blur(); return; }
521
- if (this.state.isStreaming) {
522
- const cancelBtn = document.querySelector('#cancelBtn, [data-cancel-btn]');
523
- if (cancelBtn && cancelBtn.offsetParent !== null) cancelBtn.click();
524
- }
525
- }
526
- });
527
-
528
- const themeToggle = document.querySelector('[data-theme-toggle]');
529
- if (themeToggle) {
530
- themeToggle.addEventListener('click', () => this.toggleTheme());
531
- }
532
-
533
- if (this.ui.outputEl) {
534
- this.ui.outputEl.addEventListener('click', async (e) => {
535
- const editBtn = e.target.closest('[data-edit-msg]');
536
- if (!editBtn || !this.state.currentConversation) return;
537
- const msgEl = editBtn.closest('.message-user');
538
- const textEl = msgEl?.querySelector('.message-text');
539
- if (!textEl) return;
540
- const original = textEl.textContent || '';
541
- const edited = await window.UIDialog?.prompt('Edit message:', original, 'Edit & Re-run');
542
- if (!edited || edited === original) return;
543
- this.ui.messageInput.value = edited;
544
- this.startExecution();
545
- });
546
- }
547
-
548
- this.setupScrollTracking();
549
-
550
- window.addEventListener('create-new-conversation', (event) => {
551
- this.unlockAgentAndModel();
552
- const detail = event.detail || {};
553
- this.createNewConversation(detail.workingDirectory, detail.title);
554
- });
555
-
556
- window.addEventListener('preparing-new-conversation', () => {
557
- this.unlockAgentAndModel();
558
- });
559
-
560
- window.addEventListener('conversation-selected', async (event) => {
561
- const conversationId = event.detail.conversationId;
562
- if (this._isLoadingConversation && this._loadingConversationId === conversationId) return;
563
- this._loadingConversationId = conversationId;
564
- this.updateUrlForConversation(conversationId);
565
- this._isLoadingConversation = true;
566
- try {
567
- await this.loadConversationMessages(conversationId);
568
- } finally {
569
- this._isLoadingConversation = false;
570
- this._loadingConversationId = null;
571
- }
572
- });
573
-
574
- window.addEventListener('conversation-deselected', () => {
575
- window.ConversationState?.clear('deselected');
576
- this.state.currentConversation = null;
577
- this.state.currentSession = null;
578
- this.updateUrlForConversation(null);
579
- this.enableControls();
580
- this._showWelcomeScreen();
581
- if (this.ui.messageInput) {
582
- this.ui.messageInput.value = '';
583
- this.ui.messageInput.style.height = 'auto';
584
- }
585
- this.unlockAgentAndModel();
586
- });
587
- }
588
-
589
- setupChatMicButton() {
590
- const chatMicBtn = document.getElementById('chatMicBtn');
591
- if (!chatMicBtn) return;
592
-
593
- let isRecording = false;
594
-
595
- const startRecording = async () => {
596
- if (isRecording) return;
597
- isRecording = true;
598
- chatMicBtn.classList.add('recording');
599
- const result = await window.STTHandler.startRecording();
600
- if (!result.success) {
601
- isRecording = false;
602
- chatMicBtn.classList.remove('recording');
603
- alert('Microphone access denied: ' + result.error);
604
- }
605
- };
606
-
607
- const stopRecording = async () => {
608
- if (!isRecording) return;
609
- isRecording = false;
610
- chatMicBtn.classList.remove('recording');
611
- const result = await window.STTHandler.stopRecording();
612
- if (result.success) {
613
- if (this.ui.messageInput) {
614
- this.ui.messageInput.value = result.text;
615
- }
616
- } else {
617
- alert('Transcription failed: ' + result.error);
618
- }
619
- };
620
-
621
- chatMicBtn.addEventListener('mousedown', (e) => {
622
- e.preventDefault();
623
- startRecording();
624
- });
625
-
626
- chatMicBtn.addEventListener('mouseup', (e) => {
627
- e.preventDefault();
628
- stopRecording();
629
- });
630
-
631
- chatMicBtn.addEventListener('mouseleave', (e) => {
632
- if (isRecording) {
633
- stopRecording();
634
- }
635
- });
636
-
637
- chatMicBtn.addEventListener('touchstart', (e) => {
638
- e.preventDefault();
639
- startRecording();
640
- });
641
-
642
- chatMicBtn.addEventListener('touchend', (e) => {
643
- e.preventDefault();
644
- stopRecording();
645
- });
646
-
647
- chatMicBtn.addEventListener('touchcancel', (e) => {
648
- if (isRecording) {
649
- stopRecording();
650
- }
651
- });
652
- }
653
-
654
- async connectWebSocket() {
655
- try {
656
- await this.wsManager.connect();
657
- this.updateConnectionStatus('connected');
658
- } catch (error) {
659
- console.error('WebSocket connection failed:', error);
660
- this.updateConnectionStatus('error');
661
- throw error;
662
- }
663
- }
664
-
665
- handleWebSocketMessage(data) {
666
- try {
667
- window.dispatchEvent(new CustomEvent('ws-message', { detail: data }));
668
-
669
- switch (data.type) {
670
- case 'streaming_start':
671
- this.handleStreamingStart(data).catch(e => console.error('handleStreamingStart error:', e));
672
- break;
673
- case 'streaming_resumed':
674
- this.handleStreamingResumed(data).catch(e => console.error('handleStreamingResumed error:', e));
675
- break;
676
- case 'streaming_progress':
677
- this.handleStreamingProgress(data);
678
- break;
679
- case 'streaming_complete':
680
- this.handleStreamingComplete(data);
681
- break;
682
- case 'streaming_error':
683
- this.handleStreamingError(data);
684
- break;
685
- case 'conversation_created':
686
- this.handleConversationCreated(data);
687
- break;
688
- case 'all_conversations_deleted':
689
- this.handleAllConversationsDeleted(data);
690
- break;
691
- case 'message_created':
692
- this.handleMessageCreated(data);
693
- break;
694
- case 'conversation_updated':
695
- this.handleConversationUpdated(data);
696
- break;
697
- case 'queue_status':
698
- this.handleQueueStatus(data);
699
- break;
700
- case 'queue_updated':
701
- this.handleQueueUpdated(data);
702
- break;
703
- case 'queue_item_dequeued':
704
- this.handleQueueItemDequeued(data);
705
- break;
706
- case 'rate_limit_hit':
707
- this.handleRateLimitHit(data);
708
- break;
709
- case 'rate_limit_clear':
710
- this.handleRateLimitClear(data);
711
- break;
712
- case 'model_download_progress':
713
- this._handleModelDownloadProgress(data.progress || data);
714
- break;
715
- case 'tts_setup_progress':
716
- this._handleTTSSetupProgress(data);
717
- break;
718
- default:
719
- break;
720
- }
721
- } catch (error) {
722
- console.error('Message handling error:', error);
723
- }
724
- }
725
-
726
- queueEvent(data) {
727
- try {
728
- const processed = this.eventProcessor.processEvent(data);
729
- if (!processed) return;
730
- if (data.sessionId && this.state.currentSession?.id === data.sessionId) {
731
- this.state.sessionEvents.push(processed);
732
- }
733
- } catch (error) {
734
- console.error('Event queuing error:', error);
735
- }
736
- }
737
-
738
- async handleStreamingStart(data) {
739
- this._dbg('Streaming started:', data);
740
- if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'STREAMING', conversationId: data.conversationId });
741
- this._clearThinkingCountdown();
742
- if (this._lastSendTime > 0) {
743
- const actual = Date.now() - this._lastSendTime;
744
- const predicted = this.wsManager?.latency?.predicted || 0;
745
- const serverTime = Math.max(500, actual - predicted);
746
- this._serverProcessingEstimate = 0.7 * this._serverProcessingEstimate + 0.3 * serverTime;
747
- }
748
-
749
- if (this.wsManager.isConnected) {
750
- this.wsManager.subscribeToSession(data.sessionId);
751
- if (!data.resumed) {
752
- this.wsManager.sendMessage({ type: 'subscribe', conversationId: data.conversationId });
753
- }
754
- }
755
-
756
- if (this.state.currentConversation?.id !== data.conversationId) {
757
- this._dbg('Streaming started for non-active conversation:', data.conversationId);
758
- this._setConvStreaming(data.conversationId, true, data.sessionId, data.agentId);
759
- this._dbg('[SYNC] streaming_start - non-active conv:', { convId: data.conversationId, sessionId: data.sessionId, streamingCount: this.state.streamingConversations.size });
760
- this.updateBusyPromptArea(data.conversationId);
761
- this.emit('streaming:start', data);
762
-
763
- if (!this.state.currentConversation && !this._isLoadingConversation) {
764
- this._isLoadingConversation = true;
765
- this.loadConversationMessages(data.conversationId).finally(() => {
766
- this._isLoadingConversation = false;
767
- });
768
- }
769
- return;
770
- }
771
-
772
- this._setConvStreaming(data.conversationId, true, data.sessionId, data.agentId);
773
- this.updateBusyPromptArea(data.conversationId);
774
- this.state.currentSession = {
775
- id: data.sessionId,
776
- conversationId: data.conversationId,
777
- agentId: data.agentId,
778
- startTime: Date.now()
779
- };
780
- this.state.sessionEvents = [];
781
-
782
- this.updateUrlForConversation(data.conversationId, data.sessionId);
783
-
784
- if (this.wsManager.isConnected) {
785
- this.wsManager.subscribeToSession(data.sessionId);
786
- }
787
-
788
- const outputEl = document.getElementById('output');
789
- if (outputEl) {
790
- let messagesEl = outputEl.querySelector('.conversation-messages');
791
- if (!messagesEl) {
792
- const conv = this.state.currentConversation;
793
- const wdInfo = conv?.workingDirectory ? `${this.escapeHtml(conv.workingDirectory)}` : '';
794
- const timestamp = new Date(conv?.created_at || Date.now()).toLocaleDateString();
795
- const metaParts = [timestamp];
796
- if (conv?.agentType || conv?.agentId) metaParts.push(this.escapeHtml(conv.agentType || conv.agentId));
797
- if (conv?.messageCount > 0) metaParts.push(`${conv.messageCount} msgs`);
798
- if (wdInfo) metaParts.push(wdInfo);
799
- outputEl.innerHTML = `
800
- <div class="conversation-header">
801
- <h2>${this.escapeHtml(conv?.title || 'Conversation')}</h2>
802
- <p class="text-secondary">${metaParts.join(' - ')}</p>
803
- </div>
804
- <div class="conversation-messages"></div>
805
- `;
806
- messagesEl = outputEl.querySelector('.conversation-messages');
807
- }
808
- let streamingDiv = document.getElementById(`streaming-${data.sessionId}`);
809
- if (!streamingDiv) {
810
- streamingDiv = document.createElement('div');
811
- streamingDiv.className = 'message message-assistant streaming-message';
812
- streamingDiv.id = `streaming-${data.sessionId}`;
813
- streamingDiv.innerHTML = `
814
- <div class="message-role">Assistant</div>
815
- <div class="message-blocks streaming-blocks"></div>
816
- <div class="streaming-indicator" style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;">
817
- <span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
818
- <span class="streaming-indicator-label">Thinking...</span>
819
- </div>
820
- `;
821
- messagesEl.appendChild(streamingDiv);
822
- } else {
823
- streamingDiv.classList.add('streaming-message');
824
- streamingDiv.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
825
- const indDiv = document.createElement('div');
826
- indDiv.className = 'streaming-indicator';
827
- indDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
828
- indDiv.innerHTML = `<span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span><span class="streaming-indicator-label">Thinking...</span>`;
829
- streamingDiv.appendChild(indDiv);
830
- }
831
- this.scrollToBottom(true);
832
- }
833
-
834
- this._streamStartedAt = Date.now();
835
- this._sessionCost = 0;
836
- if (this._elapsedTimer) clearInterval(this._elapsedTimer);
837
- this._elapsedTimer = setInterval(() => {
838
- const label = streamingDiv?.querySelector('.streaming-indicator-label');
839
- if (label) {
840
- const sec = ((Date.now() - this._streamStartedAt) / 1000) | 0;
841
- const time = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
842
- label.textContent = this._sessionCost > 0 ? time + ' · $' + this._sessionCost.toFixed(4) : time;
843
- }
844
- }, 1000);
845
-
846
- this._renderedSeqs[data.sessionId] = new Set();
847
-
848
- this.showStreamingPromptButtons();
849
-
850
- this.emit('streaming:start', data);
851
- }
852
-
853
- async handleStreamingResumed(data) {
854
- this._dbg('Streaming resumed:', data);
855
- const conv = this.state.currentConversation || { id: data.conversationId };
856
- await this.handleStreamingStart({
857
- type: 'streaming_start',
858
- sessionId: data.sessionId,
859
- conversationId: data.conversationId,
860
- agentId: conv.agentType || conv.agentId || 'claude-code',
861
- resumed: true,
862
- timestamp: data.timestamp
863
- });
864
- }
865
-
866
- handleStreamingProgress(data) {
867
- try { return this._handleStreamingProgressInner(data); } catch (e) { console.error('[render-error] streaming progress:', e); }
868
- }
869
- _handleStreamingProgressInner(data) {
870
- if (!data.block || !data.sessionId) return;
871
-
872
- const seen = this._renderedSeqs[data.sessionId] || (this._renderedSeqs[data.sessionId] = new Set());
873
- if (data.seq !== undefined) {
874
- if (seen.has(data.seq)) return;
875
- seen.add(data.seq);
876
- }
877
-
878
- const block = data.block;
879
-
880
- const convId = data.conversationId;
881
- if (convId) {
882
- let entry = this._bgCache.get(convId);
883
- if (!entry) {
884
- if (this._bgCache.size >= this.BG_CACHE_MAX) {
885
- const oldestKey = this._bgCache.keys().next().value;
886
- this._bgCache.delete(oldestKey);
887
- }
888
- entry = { items: [], seqSet: new Set(), sessionId: data.sessionId };
889
- this._bgCache.set(convId, entry);
890
- }
891
- if (data.seq === undefined || !entry.seqSet.has(data.seq)) {
892
- if (data.seq !== undefined) entry.seqSet.add(data.seq);
893
- entry.sessionId = data.sessionId;
894
- try {
895
- const packed = typeof msgpackr !== 'undefined' ? msgpackr.pack(block) : block;
896
- entry.items.push({ seq: data.seq, packed });
897
- } catch (_) { entry.items.push({ seq: data.seq, packed: block }); }
898
- }
899
- }
900
-
901
- if (this.state.currentSession?.id !== data.sessionId) return;
902
-
903
- const streamingEl = document.getElementById(`streaming-${data.sessionId}`);
904
- if (!streamingEl) return;
905
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
906
- if (!blocksEl) return;
907
-
908
- if (block.type === 'tool_result') {
909
- const tid = block.tool_use_id;
910
- const toolUseEl = (tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`))
911
- || (blocksEl.lastElementChild?.classList?.contains('block-tool-use') ? blocksEl.lastElementChild : null);
912
- if (toolUseEl) {
913
- this.renderer.mergeResultIntoToolUse(toolUseEl, block);
914
- this.scrollToBottom();
915
- return;
916
- }
917
- }
918
-
919
- if (block.type === 'tool_status') {
920
- const tid = block.tool_use_id;
921
- const toolUseEl = tid && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${tid}"]`);
922
- if (toolUseEl) {
923
- const summary = toolUseEl.querySelector(':scope > summary');
924
- if (summary) {
925
- let badge = summary.querySelector('.folded-tool-status-live');
926
- if (!badge) { badge = document.createElement('span'); badge.className = 'folded-tool-status-live'; summary.appendChild(badge); }
927
- const isRunning = block.status === 'in_progress';
928
- badge.style.cssText = 'margin-left:auto;font-size:0.65rem;opacity:0.7;display:inline-flex;align-items:center;gap:0.25rem';
929
- badge.innerHTML = (isRunning ? '<span class="animate-spin" style="display:inline-block;width:0.6rem;height:0.6rem;border:1.5px solid var(--color-border);border-top-color:var(--color-info);border-radius:50%"></span>' : '')
930
- + '<span>' + (isRunning ? 'Running' : (block.status || '')) + '</span>';
931
- }
932
- this.scrollToBottom();
933
- return;
934
- }
935
- }
936
-
937
- if (block.type === 'hook_progress') {
938
- const hookEvent = (block.hookEvent || '').split(':')[0];
939
- if (hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') {
940
- const lastTool = blocksEl.querySelector('.block-tool-use:last-of-type') || blocksEl.lastElementChild;
941
- if (lastTool?.classList?.contains('block-tool-use')) {
942
- const summary = lastTool.querySelector(':scope > summary');
943
- if (summary) {
944
- let hookBadge = summary.querySelector('.folded-tool-hook');
945
- if (!hookBadge) { hookBadge = document.createElement('span'); hookBadge.className = 'folded-tool-hook'; hookBadge.style.cssText = 'margin-left:0.375rem;font-size:0.6rem;opacity:0.4;font-weight:400'; summary.appendChild(hookBadge); }
946
- hookBadge.textContent = block.hookEvent?.split(':').pop() || hookEvent;
947
- }
948
- return;
949
- }
950
- }
951
- return;
952
- }
953
-
954
- if (block.type === 'result' && block.total_cost_usd) {
955
- this._sessionCost = (this._sessionCost || 0) + block.total_cost_usd;
956
- }
957
-
958
- const el = this.renderer.renderBlock(block, data, blocksEl);
959
- if (el) {
960
- blocksEl.appendChild(el);
961
- this.scrollToBottom();
962
- }
963
- }
964
-
965
- renderBlockContent(block) {
966
- if (block.type === 'text' && block.text) {
967
- const text = block.text;
968
- if (this.isHtmlContent(text)) {
969
- return `<div class="html-content">${this.sanitizeHtml(text)}</div>`;
970
- }
971
- const parts = this.parseMarkdownCodeBlocks(text);
972
- if (parts.length === 1 && parts[0].type === 'text') {
973
- return this.escapeHtml(text);
974
- }
975
- return parts.map(part => {
976
- if (part.type === 'html') {
977
- return `<div class="html-content">${this.sanitizeHtml(part.content)}</div>`;
978
- } else if (part.type === 'code') {
979
- return this.renderCodeBlock(part.language, part.code);
980
- }
981
- return this.escapeHtml(part.content);
982
- }).join('');
983
- }
984
- const fieldsHtml = Object.entries(block)
985
- .filter(([key]) => key !== 'type')
986
- .map(([key, value]) => {
987
- let displayValue = typeof value === 'string' ? value : JSON.stringify(value);
988
- if (displayValue.length > 100) displayValue = displayValue.substring(0, 100) + '...';
989
- return `<div style="font-size:0.75rem;margin-bottom:0.25rem"><span style="font-weight:600">${this.escapeHtml(key)}:</span> <code>${this.escapeHtml(displayValue)}</code></div>`;
990
- }).join('');
991
- return `<div style="padding:0.5rem;background:var(--color-bg-secondary);border-radius:0.375rem;border:1px solid var(--color-border)"><div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;margin-bottom:0.25rem">${this.escapeHtml(block.type)}</div>${fieldsHtml}</div>`;
992
- }
993
-
994
- scrollToBottom(force = false) {
995
- const scrollContainer = document.getElementById('output-scroll');
996
- if (!scrollContainer) return;
997
- const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
998
-
999
- if (this._userScrolledUp && !force) {
1000
- this._unseenCount = (this._unseenCount || 0) + 1;
1001
- this._showNewContentPill();
1002
- return;
1003
- }
1004
-
1005
- if (!force && distFromBottom > 150) {
1006
- this._unseenCount = (this._unseenCount || 0) + 1;
1007
- this._showNewContentPill();
1008
- return;
1009
- }
1010
-
1011
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
1012
- this._removeNewContentPill();
1013
- this._scrollAnimating = false;
1014
- }
1015
-
1016
- _showNewContentPill() {
1017
- let pill = document.getElementById('new-content-pill');
1018
- const scrollContainer = document.getElementById('output-scroll');
1019
- if (!scrollContainer) return;
1020
- if (!pill) {
1021
- pill = document.createElement('button');
1022
- pill.id = 'new-content-pill';
1023
- pill.className = 'new-content-pill';
1024
- pill.addEventListener('click', () => {
1025
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
1026
- this._removeNewContentPill();
1027
- });
1028
- scrollContainer.appendChild(pill);
1029
- }
1030
- pill.textContent = (this._unseenCount || 1) + ' new';
1031
- }
1032
-
1033
- _removeNewContentPill() {
1034
- this._unseenCount = 0;
1035
- const pill = document.getElementById('new-content-pill');
1036
- if (pill) pill.remove();
1037
- }
1038
-
1039
- handleStreamingError(data) {
1040
- console.error('Streaming error:', data);
1041
- if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
1042
- this._clearThinkingCountdown();
1043
- if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
1044
-
1045
- if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
1046
- if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
1047
- if (this.ui.sendButton) this.ui.sendButton.style.display = '';
1048
-
1049
- const conversationId = data.conversationId || this.state.currentSession?.conversationId;
1050
-
1051
- if (conversationId && this.state.currentConversation?.id !== conversationId) {
1052
- this._dbg('Streaming error for non-active conversation:', conversationId);
1053
- this._setConvStreaming(conversationId, false);
1054
- this.updateBusyPromptArea(conversationId);
1055
- this.emit('streaming:error', data);
1056
- return;
1057
- }
1058
-
1059
- this._setConvStreaming(conversationId, false);
1060
- this.updateBusyPromptArea(conversationId);
1061
-
1062
- const queueEl = document.querySelector('.queue-indicator');
1063
- if (queueEl) queueEl.remove();
1064
-
1065
- if (data.isPrematureEnd) {
1066
- this.renderer.queueEvent({
1067
- type: 'streaming_error',
1068
- isPrematureEnd: true,
1069
- exitCode: data.exitCode,
1070
- error: data.error,
1071
- stderrText: data.stderrText,
1072
- sessionId: data.sessionId,
1073
- conversationId: data.conversationId,
1074
- timestamp: data.timestamp || Date.now()
1075
- });
1076
- }
1077
-
1078
- const sessionId = data.sessionId || this.state.currentSession?.id;
1079
-
1080
- const outputEl2 = document.getElementById('output');
1081
- if (outputEl2) {
1082
- outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => {
1083
- ind.innerHTML = `<span style="color:var(--color-error);">Error: ${this.escapeHtml(data.error || 'Unknown error')}</span>`;
1084
- });
1085
- }
1086
-
1087
- const streamingEl = document.getElementById(`streaming-${sessionId}`);
1088
- if (streamingEl) {
1089
- streamingEl.classList.remove('streaming-message');
1090
- const indicator = streamingEl.querySelector('.streaming-indicator');
1091
- if (indicator) {
1092
- indicator.innerHTML = `<span style="color:var(--color-error);">Error: ${this.escapeHtml(data.error || 'Unknown error')}</span>`;
1093
- }
1094
- streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
1095
- } else {
1096
- const outputEl3 = document.getElementById('output');
1097
- const messagesEl3 = outputEl3 && outputEl3.querySelector('.conversation-messages');
1098
- if (messagesEl3 && data.error) {
1099
- const errDiv = document.createElement('div');
1100
- errDiv.className = 'message';
1101
- errDiv.style = 'padding:0.75rem;border:1px solid var(--color-error, #e53e3e);border-radius:4px;margin:0.5rem 0;';
1102
- errDiv.innerHTML = `<span style="color:var(--color-error, #e53e3e);">Error: ${this.escapeHtml(data.error)}</span>`;
1103
- messagesEl3.appendChild(errDiv);
1104
- }
1105
- }
1106
-
1107
- this.unlockAgentAndModel();
1108
- this.enableControls();
1109
- this.emit('streaming:error', data);
1110
- }
1111
-
1112
- handleStreamingComplete(data) {
1113
- this._dbg('Streaming completed:', data);
1114
- if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
1115
- this._clearThinkingCountdown();
1116
- if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
1117
-
1118
- const conversationId = data.conversationId || this.state.currentSession?.conversationId;
1119
- if (conversationId) this.invalidateCache(conversationId);
1120
-
1121
- if (conversationId && this.state.currentConversation?.id !== conversationId) {
1122
- this._dbg('Streaming completed for non-active conversation:', conversationId);
1123
- this._setConvStreaming(conversationId, false);
1124
- this._dbg('[SYNC] streaming_complete - non-active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size });
1125
- this.updateBusyPromptArea(conversationId);
1126
- this.emit('streaming:complete', data);
1127
- return;
1128
- }
1129
-
1130
- this._setConvStreaming(conversationId, false);
1131
- this._dbg('[SYNC] streaming_complete - active conv:', { convId: conversationId, streamingCount: this.state.streamingConversations.size, interrupted: data.interrupted });
1132
- this.updateBusyPromptArea(conversationId);
1133
-
1134
- const sessionId = data.sessionId || this.state.currentSession?.id;
1135
-
1136
- if (sessionId && this.wsManager) {
1137
- try {
1138
- this.wsManager.unsubscribeFromSession(sessionId);
1139
- } catch (e) {
1140
- }
1141
- }
1142
-
1143
- const queueEl = document.querySelector('.queue-indicator');
1144
- if (queueEl) queueEl.remove();
1145
-
1146
- const outputEl2 = document.getElementById('output');
1147
- if (outputEl2) {
1148
- outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
1149
- outputEl2.querySelectorAll('.event-streaming-start, .event-streaming-complete').forEach(block => block.remove());
1150
- }
1151
- const streamingEl = document.getElementById(`streaming-${sessionId}`);
1152
- if (streamingEl) {
1153
- streamingEl.classList.remove('streaming-message');
1154
- const prevTextEl = streamingEl.querySelector('.streaming-text-current');
1155
- if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
1156
-
1157
- streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
1158
-
1159
- const ts = document.createElement('div');
1160
- ts.className = 'message-timestamp';
1161
- ts.textContent = new Date().toLocaleString();
1162
- streamingEl.appendChild(ts);
1163
- }
1164
-
1165
- if (conversationId) {
1166
- this.saveScrollPosition(conversationId);
1167
- }
1168
-
1169
- this._recoverMissedChunks().catch(err => {
1170
- console.warn('Chunk recovery failed:', err.message);
1171
- });
1172
-
1173
- this.enableControls();
1174
- this.emit('streaming:complete', data);
1175
-
1176
- this._promptPushIfWeOwnRemote();
1177
- }
1178
-
1179
- async _promptPushIfWeOwnRemote() {
1180
- try {
1181
- const { ownsRemote, hasChanges, hasUnpushed, remoteUrl } = await window.wsClient.rpc('git.check');
1182
- if (ownsRemote && (hasChanges || hasUnpushed)) {
1183
- const conv = this.state.currentConversation;
1184
- if (conv) {
1185
- this.streamToConversation(conv.id, 'Push the changes to the remote repository.', conv.agentId);
1186
- }
1187
- }
1188
- } catch (e) {
1189
- console.warn('Auto-push check failed:', e);
1190
- }
1191
- }
1192
-
1193
- handleConversationCreated(data) {
1194
- if (data.conversation) {
1195
- if (this.state.conversations.some(c => c.id === data.conversation.id)) {
1196
- return;
1197
- }
1198
- this.state.conversations.push(data.conversation);
1199
- this.emit('conversation:created', data.conversation);
1200
- }
1201
- }
1202
-
1203
- handleMessageCreated(data) {
1204
- if (data.conversationId !== this.state.currentConversation?.id || !data.message) {
1205
- this.emit('message:created', data);
1206
- return;
1207
- }
1208
-
1209
- this._dbg('[SYNC] message_created:', { msgId: data.message.id, role: data.message.role, convId: data.conversationId });
1210
-
1211
- if (data.message.role === 'user' && this.state.currentConversation) {
1212
- this.state.currentConversation.messageCount = (this.state.currentConversation.messageCount || 0) + 1;
1213
- }
1214
-
1215
- if (data.message.role === 'assistant' && this._convIsStreaming(data.conversationId)) {
1216
- this.emit('message:created', data);
1217
- return;
1218
- }
1219
-
1220
- const outputEl = document.querySelector('.conversation-messages');
1221
- if (!outputEl) {
1222
- this.emit('message:created', data);
1223
- return;
1224
- }
1225
-
1226
- if (data.message.role === 'user') {
1227
- const pending = outputEl.querySelector('.message-sending');
1228
- if (pending) {
1229
- pending.id = '';
1230
- pending.setAttribute('data-msg-id', data.message.id);
1231
- pending.classList.remove('message-sending');
1232
- const ts = pending.querySelector('.message-timestamp');
1233
- if (ts) {
1234
- ts.style.opacity = '1';
1235
- ts.textContent = new Date(data.message.created_at).toLocaleString();
1236
- }
1237
- this.emit('message:created', data);
1238
- return;
1239
- }
1240
- const pendingById = outputEl.querySelector('[id^="pending-"]');
1241
- if (pendingById) {
1242
- pendingById.id = '';
1243
- pendingById.setAttribute('data-msg-id', data.message.id);
1244
- const ts = pendingById.querySelector('.message-timestamp');
1245
- if (ts) {
1246
- ts.style.opacity = '1';
1247
- ts.textContent = new Date(data.message.created_at).toLocaleString();
1248
- }
1249
- this.emit('message:created', data);
1250
- return;
1251
- }
1252
- const existingMsg = outputEl.querySelector(`[data-msg-id="${data.message.id}"]`);
1253
- if (existingMsg) {
1254
- this.emit('message:created', data);
1255
- return;
1256
- }
1257
- }
1258
-
1259
- outputEl.querySelectorAll('p.text-secondary').forEach(p => p.remove());
1260
- const safeRole = this.escapeHtml(data.message.role || 'user');
1261
- const safeId = this.escapeHtml(data.message.id || '');
1262
- const messageHtml = `
1263
- <div class="message message-${safeRole}" data-msg-id="${safeId}">
1264
- <div class="message-role">${safeRole.charAt(0).toUpperCase() + safeRole.slice(1)}</div>
1265
- ${this.renderMessageContent(data.message.content)}
1266
- <div class="message-timestamp">${new Date(data.message.created_at).toLocaleString()}</div>
1267
- </div>
1268
- `;
1269
- outputEl.insertAdjacentHTML('beforeend', messageHtml);
1270
- this.scrollToBottom();
1271
- this.emit('message:created', data);
1272
- }
1273
-
1274
- handleConversationUpdated(data) {
1275
- if (data.conversation && data.conversation.id === this.state.currentConversation?.id) {
1276
- this.state.currentConversation = data.conversation;
1277
- }
1278
- this.emit('conversation:updated', data);
1279
- }
1280
-
1281
- handleQueueStatus(data) {
1282
- if (typeof convMachineAPI !== 'undefined') convMachineAPI.send(data.conversationId, { type: 'QUEUE_UPDATE', queueLength: data.queueLength || 0 });
1283
- if (data.conversationId !== this.state.currentConversation?.id) return;
1284
- this.fetchAndRenderQueue(data.conversationId);
1285
- }
1286
-
1287
- handleQueueUpdated(data) {
1288
- if (data.conversationId !== this.state.currentConversation?.id) return;
1289
- this.fetchAndRenderQueue(data.conversationId);
1290
- }
1291
-
1292
- handleQueueItemDequeued(data) {
1293
- if (data.conversationId !== this.state.currentConversation?.id) return;
1294
- this.fetchAndRenderQueue(data.conversationId);
1295
- }
1296
-
1297
- async fetchAndRenderQueue(conversationId) {
1298
- const outputEl = document.querySelector('.conversation-messages');
1299
- if (!outputEl) return;
1300
-
1301
- try {
1302
- const { queue } = await window.wsClient.rpc('q.ls', { id: conversationId });
1303
-
1304
- let queueEl = outputEl.querySelector('.queue-indicator');
1305
- if (!queue || queue.length === 0) {
1306
- if (queueEl) queueEl.remove();
1307
- return;
1308
- }
1309
-
1310
- if (!queueEl) {
1311
- queueEl = document.createElement('div');
1312
- queueEl.className = 'queue-indicator';
1313
- outputEl.appendChild(queueEl);
1314
- }
1315
-
1316
- queueEl.innerHTML = queue.map((q, i) => `
1317
- <div class="queue-item" data-message-id="${q.messageId}" style="padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;display:flex;align-items:center;gap:0.5rem;">
1318
- <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${i + 1}. ${this.escapeHtml(q.content)}</span>
1319
- <button class="queue-edit-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Edit</button>
1320
- <button class="queue-delete-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Delete</button>
1321
- </div>
1322
- `).join('');
1323
-
1324
- if (!queueEl._listenersAttached) {
1325
- queueEl._listenersAttached = true;
1326
- queueEl.addEventListener('click', async (e) => {
1327
- if (e.target.classList.contains('queue-delete-btn')) {
1328
- const index = parseInt(e.target.dataset.index);
1329
- const msgId = queue[index].messageId;
1330
- if (await window.UIDialog.confirm('Delete this queued message?', 'Delete Message')) {
1331
- await window.wsClient.rpc('q.del', { id: conversationId, messageId: msgId });
1332
- }
1333
- } else if (e.target.classList.contains('queue-edit-btn')) {
1334
- const index = parseInt(e.target.dataset.index);
1335
- const q = queue[index];
1336
- const newContent = await window.UIDialog.prompt('Edit message:', q.content, 'Edit Queued Message');
1337
- if (newContent !== null && newContent !== q.content) {
1338
- window.wsClient.rpc('q.upd', { id: conversationId, messageId: q.messageId, content: newContent });
1339
- }
1340
- }
1341
- });
1342
- }
1343
- } catch (err) {
1344
- console.error('Failed to fetch queue:', err);
1345
- }
1346
- }
1347
-
1348
- handleRateLimitHit(data) {
1349
- if (data.conversationId !== this.state.currentConversation?.id) return;
1350
- this._setConvStreaming(data.conversationId, false);
1351
-
1352
- this.enableControls();
1353
-
1354
- const cooldownMs = data.retryAfterMs || 60000;
1355
- this._rateLimitSafetyTimer = setTimeout(() => {
1356
- this.enableControls();
1357
- }, cooldownMs + 10000);
1358
-
1359
- const sessionId = data.sessionId || this.state.currentSession?.id;
1360
- const streamingEl = document.getElementById(`streaming-${sessionId}`);
1361
- if (streamingEl) {
1362
- const indicator = streamingEl.querySelector('.streaming-indicator');
1363
- if (indicator) {
1364
- const retrySeconds = Math.ceil(cooldownMs / 1000);
1365
- indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${retrySeconds}s...</span>`;
1366
- let remaining = retrySeconds;
1367
- const countdownTimer = setInterval(() => {
1368
- remaining--;
1369
- if (remaining <= 0) {
1370
- clearInterval(countdownTimer);
1371
- indicator.innerHTML = '<span style="color:var(--color-info);">Restarting...</span>';
1372
- } else {
1373
- indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${remaining}s...</span>`;
1374
- }
1375
- }, 1000);
1376
- }
1377
- }
1378
- }
1379
-
1380
- handleRateLimitClear(data) {
1381
- if (data.conversationId !== this.state.currentConversation?.id) return;
1382
- if (this._rateLimitSafetyTimer) {
1383
- clearTimeout(this._rateLimitSafetyTimer);
1384
- this._rateLimitSafetyTimer = null;
1385
- }
1386
- this.enableControls();
1387
- }
1388
-
1389
- handleAllConversationsDeleted(data) {
1390
- window.ConversationState?.clear('all_deleted');
1391
- this.state.currentConversation = null;
1392
- this.state.conversations = [];
1393
- this.state.sessionEvents = [];
1394
- this.conversationCache.clear();
1395
- this.conversationListCache = { data: [], timestamp: 0, ttl: 30000 };
1396
- this.draftPrompts.clear();
1397
- window.dispatchEvent(new CustomEvent('conversation-deselected'));
1398
- const outputEl = document.getElementById('output');
1399
- if (outputEl) outputEl.innerHTML = '';
1400
- }
1401
-
1402
- isHtmlContent(text) {
1403
- const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
1404
- return htmlPattern.test(text);
1405
- }
1406
-
1407
- sanitizeHtml(html) {
1408
- const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
1409
- let cleaned = html.replace(dangerous, '');
1410
- cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
1411
- cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
1412
- cleaned = cleaned.replace(/javascript\s*:/gi, '');
1413
- return cleaned;
1414
- }
1415
-
1416
- parseMarkdownCodeBlocks(text) {
1417
- const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
1418
- const parts = [];
1419
- let lastIndex = 0;
1420
- let match;
1421
-
1422
- while ((match = codeBlockRegex.exec(text)) !== null) {
1423
- if (match.index > lastIndex) {
1424
- const segment = text.substring(lastIndex, match.index);
1425
- parts.push({
1426
- type: this.isHtmlContent(segment) ? 'html' : 'text',
1427
- content: segment
1428
- });
1429
- }
1430
- parts.push({
1431
- type: 'code',
1432
- language: match[1] || 'plain',
1433
- code: match[2]
1434
- });
1435
- lastIndex = codeBlockRegex.lastIndex;
1436
- }
1437
-
1438
- if (lastIndex < text.length) {
1439
- const segment = text.substring(lastIndex);
1440
- parts.push({
1441
- type: this.isHtmlContent(segment) ? 'html' : 'text',
1442
- content: segment
1443
- });
1444
- }
1445
-
1446
- if (parts.length === 0) {
1447
- return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
1448
- }
1449
-
1450
- return parts;
1451
- }
1452
-
1453
- renderCodeBlock(language, code) {
1454
- if (language.toLowerCase() === 'html') {
1455
- return `
1456
- <div class="message-code">
1457
- <div class="html-rendered-label">
1458
- Rendered HTML
1459
- </div>
1460
- <div class="html-content">
1461
- ${this.sanitizeHtml(code)}
1462
- </div>
1463
- </div>
1464
- `;
1465
- } else {
1466
- const lineCount = code.split('\n').length;
1467
- return `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(code)}</pre></details></div>`;
1468
- }
1469
- }
1470
-
1471
- renderMessageContent(content) {
1472
- if (typeof content === 'string') {
1473
- if (this.isHtmlContent(content)) {
1474
- return `<div class="message-text"><div class="html-content">${this.sanitizeHtml(content)}</div></div>`;
1475
- }
1476
- return `<div class="message-text">${this.escapeHtml(content)}</div>`;
1477
- } else if (content && typeof content === 'object' && content.type === 'claude_execution') {
1478
- let html = '<div class="message-blocks">';
1479
- if (content.blocks && Array.isArray(content.blocks)) {
1480
- let pendingToolUseClose = false;
1481
- let pendingHasInput = false;
1482
- content.blocks.forEach((block, blockIdx, blocks) => {
1483
- if (block.type !== 'tool_result' && pendingToolUseClose) {
1484
- if (pendingHasInput) html += '</div>';
1485
- html += '</details>';
1486
- pendingToolUseClose = false;
1487
- pendingHasInput = false;
1488
- }
1489
- if (block.type === 'text') {
1490
- const parts = this.parseMarkdownCodeBlocks(block.text);
1491
- parts.forEach(part => {
1492
- if (part.type === 'html') {
1493
- html += `<div class="message-text"><div class="html-content">${this.sanitizeHtml(part.content)}</div></div>`;
1494
- } else if (part.type === 'text') {
1495
- html += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
1496
- } else if (part.type === 'code') {
1497
- html += this.renderCodeBlock(part.language, part.code);
1498
- }
1499
- });
1500
- } else if (block.type === 'code_block') {
1501
- if (block.language === 'html') {
1502
- html += `
1503
- <div class="message-code">
1504
- <div class="html-rendered-label">
1505
- Rendered HTML
1506
- </div>
1507
- <div class="html-content">
1508
- ${this.sanitizeHtml(block.code)}
1509
- </div>
1510
- </div>
1511
- `;
1512
- } else {
1513
- const blkLineCount = block.code.split('\n').length;
1514
- html += `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(block.language || 'code')} - ${blkLineCount} line${blkLineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(block.code)}</pre></details></div>`;
1515
- }
1516
- } else if (block.type === 'tool_use') {
1517
- let inputContentHtml = '';
1518
- const hasInput = block.input && Object.keys(block.input).length > 0;
1519
- if (hasInput) {
1520
- const inputStr = JSON.stringify(block.input, null, 2);
1521
- inputContentHtml = `<pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre>`;
1522
- }
1523
- const tn = block.name || 'unknown';
1524
- const hasRenderer = typeof StreamingRenderer !== 'undefined';
1525
- const dName = hasRenderer ? StreamingRenderer.getToolDisplayName(tn) : tn;
1526
- const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
1527
- const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
1528
- const typeClass = hasRenderer && this.renderer ? this.renderer._getBlockTypeClass('tool_use') : 'block-type-tool_use';
1529
- const toolColorClass = hasRenderer && this.renderer ? this.renderer._getToolColorClass(tn) : 'tool-color-default';
1530
- const nextBlock = blocks[blockIdx + 1];
1531
- const resultClass = nextBlock?.type === 'tool_result' ? (nextBlock.is_error ? 'has-error tool-result-error' : 'has-success tool-result-success') : '';
1532
- const resultStatusIcon = nextBlock?.type === 'tool_result'
1533
- ? `<span class="folded-tool-status">${nextBlock.is_error
1534
- ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1535
- : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>'
1536
- }</span>` : '';
1537
- if (hasInput) {
1538
- html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}${resultStatusIcon}</summary><div class="folded-tool-body">${inputContentHtml}`;
1539
- pendingHasInput = true;
1540
- } else {
1541
- html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}${resultStatusIcon}</summary>`;
1542
- pendingHasInput = false;
1543
- }
1544
- pendingToolUseClose = true;
1545
- } else if (block.type === 'tool_result') {
1546
- const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1547
- const smartHtml = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.renderSmartContentHTML(content, this.escapeHtml.bind(this), true) : `<pre class="tool-result-pre">${this.escapeHtml(content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content)}</pre>`;
1548
- const resultContentHtml = `<div class="folded-tool-result-content">${smartHtml}</div>`;
1549
- if (pendingToolUseClose) {
1550
- if (pendingHasInput) {
1551
- html += resultContentHtml + '</div></details>';
1552
- } else {
1553
- html += `<div class="folded-tool-body">${resultContentHtml}</div></details>`;
1554
- }
1555
- pendingToolUseClose = false;
1556
- } else {
1557
- html += resultContentHtml;
1558
- }
1559
- }
1560
- });
1561
- if (pendingToolUseClose) {
1562
- if (pendingHasInput) html += '</div>';
1563
- html += '</details>';
1564
- }
1565
- }
1566
- html += '</div>';
1567
- return html;
1568
- } else {
1569
- if (typeof content === 'object' && content !== null) {
1570
- const fieldsHtml = Object.entries(content)
1571
- .map(([key, value]) => {
1572
- let displayValue = typeof value === 'string' ? value : JSON.stringify(value);
1573
- if (displayValue.length > 150) displayValue = displayValue.substring(0, 150) + '...';
1574
- return `<div style="font-size:0.8rem;margin-bottom:0.375rem"><span style="font-weight:600">${this.escapeHtml(key)}:</span> <code style="background:var(--color-bg-secondary);padding:0.125rem 0.25rem;border-radius:0.25rem">${this.escapeHtml(displayValue)}</code></div>`;
1575
- }).join('');
1576
- return `<div class="message-text" style="background:var(--color-bg-secondary);padding:0.75rem;border-radius:0.375rem">${fieldsHtml}</div>`;
1577
- }
1578
- return `<div class="message-text">${this.escapeHtml(String(content))}</div>`;
1579
- }
1580
- }
1581
-
1582
- async startExecution() {
1583
- const prompt = this.ui.messageInput?.value || '';
1584
-
1585
- if (!prompt.trim()) {
1586
- this.showError('Please enter a prompt');
1587
- return;
1588
- }
1589
-
1590
- this.disableControls();
1591
- const ttsActive = window.TTSHandler && window.TTSHandler.getAutoSpeak && window.TTSHandler.getAutoSpeak();
1592
- const savedPrompt = ttsActive ? prompt + '\n\n[Respond optimized for text-to-speech: use short sentences, simple words, and focus on clarity.]' : prompt;
1593
- if (this.ui.messageInput) {
1594
- this.ui.messageInput.value = '';
1595
- this.ui.messageInput.style.height = 'auto';
1596
- }
1597
-
1598
- const pendingId = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
1599
-
1600
- const isStreaming = this._convIsStreaming(this.state.currentConversation?.id);
1601
- if (!isStreaming) {
1602
- this._showOptimisticMessage(pendingId, savedPrompt);
1603
- }
1604
-
1605
- try {
1606
- let conv = this.state.currentConversation;
1607
-
1608
- if (this._isLoadingConversation) {
1609
- this.showError('Conversation still loading. Please try again.');
1610
- this.enableControls();
1611
- return;
1612
- }
1613
-
1614
- if (conv && typeof conv === 'string') {
1615
- this.showError('Conversation state invalid. Please reload.');
1616
- this.enableControls();
1617
- return;
1618
- }
1619
-
1620
- if (conv?.id) {
1621
- const isNewConversation = !conv.messageCount && !this._convIsStreaming(conv.id);
1622
- const agentId = (isNewConversation ? this.getCurrentAgent() : null) || conv?.agentType || this.getCurrentAgent();
1623
- const subAgent = this.getEffectiveSubAgent() || conv?.subAgent || null;
1624
- const model = this.ui.modelSelector?.value || null;
1625
-
1626
- this.lockAgentAndModel(agentId, model);
1627
- await this.streamToConversation(conv.id, savedPrompt, agentId, model, subAgent);
1628
- this.clearDraft(conv.id);
1629
- if (!isStreaming) {
1630
- this._confirmOptimisticMessage(pendingId);
1631
- }
1632
- } else {
1633
- const agentId = this.getCurrentAgent();
1634
- const subAgent = this.getEffectiveSubAgent() || null;
1635
- const model = this.ui.modelSelector?.value || null;
1636
-
1637
- const body = { agentId, title: savedPrompt.substring(0, 50) };
1638
- if (model) body.model = model;
1639
- if (subAgent) body.subAgent = subAgent;
1640
- const { conversation } = await window.wsClient.rpc('conv.new', body);
1641
- window.ConversationState?.selectConversation(conversation.id, 'conversation_created', 1);
1642
- this.state.currentConversation = conversation;
1643
- this.lockAgentAndModel(agentId, model);
1644
-
1645
- if (window.conversationManager) {
1646
- window.conversationManager.loadConversations();
1647
- window.conversationManager.select(conversation.id);
1648
- }
1649
-
1650
- await this.streamToConversation(conversation.id, savedPrompt, agentId, model, subAgent);
1651
- this.clearDraft(conversation.id);
1652
- this._confirmOptimisticMessage(pendingId);
1653
- }
1654
- } catch (error) {
1655
- console.error('Execution error:', error);
1656
- if (!isStreaming) {
1657
- this._failOptimisticMessage(pendingId, savedPrompt, error.message);
1658
- }
1659
- this.enableControls();
1660
- }
1661
- }
1662
-
1663
- _showOptimisticMessage(pendingId, content) {
1664
- const messagesEl = document.querySelector('.conversation-messages');
1665
- if (!messagesEl) return;
1666
- messagesEl.querySelectorAll('p.text-secondary').forEach(p => p.remove());
1667
- const div = document.createElement('div');
1668
- div.className = 'message message-user message-sending';
1669
- div.id = pendingId;
1670
- div.innerHTML = `<div class="message-role">User</div><div class="message-text">${this.escapeHtml(content)}</div><div class="message-timestamp" style="opacity:0.5">Sending...</div>`;
1671
- messagesEl.appendChild(div);
1672
- this.scrollToBottom(true);
1673
- }
1674
-
1675
- _confirmOptimisticMessage(pendingId) {
1676
- const el = document.getElementById(pendingId);
1677
- if (!el) return;
1678
- el.classList.remove('message-sending');
1679
- const ts = el.querySelector('.message-timestamp');
1680
- if (ts) {
1681
- ts.style.opacity = '1';
1682
- ts.textContent = new Date().toLocaleString();
1683
- }
1684
- }
1685
-
1686
- _failOptimisticMessage(pendingId, content, errorMsg) {
1687
- const el = document.getElementById(pendingId);
1688
- if (!el) return;
1689
- el.classList.remove('message-sending');
1690
- el.classList.add('message-send-failed');
1691
- const ts = el.querySelector('.message-timestamp');
1692
- if (ts) {
1693
- ts.style.opacity = '1';
1694
- ts.innerHTML = `<span style="color:var(--color-error)">Failed: ${this.escapeHtml(errorMsg)}</span>`;
1695
- }
1696
- if (this.ui.messageInput) {
1697
- this.ui.messageInput.value = content;
1698
- }
1699
- }
1700
-
1701
- _subscribeToConversationUpdates() {
1702
- if (!this.state.conversations || this.state.conversations.length === 0) return;
1703
- for (const conv of this.state.conversations) {
1704
- this.wsManager.subscribeToConversation(conv.id);
1705
- }
1706
- }
1707
-
1708
- _flushBgCache(conversationId, sessionId) {
1709
- const entry = this._bgCache.get(conversationId);
1710
- if (!entry || entry.items.length === 0) return;
1711
- if (entry.sessionId !== sessionId) { this._bgCache.delete(conversationId); return; }
1712
-
1713
- const streamingEl = document.getElementById(`streaming-${sessionId}`);
1714
- if (!streamingEl) return;
1715
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
1716
- if (!blocksEl) return;
1717
-
1718
- const seenSeqs = this._renderedSeqs[sessionId] || (this._renderedSeqs[sessionId] = new Set());
1719
- for (const item of entry.items) {
1720
- if (item.seq !== undefined && seenSeqs.has(item.seq)) continue;
1721
- try {
1722
- const block = (typeof msgpackr !== 'undefined' && item.packed instanceof Uint8Array)
1723
- ? msgpackr.unpack(item.packed) : item.packed;
1724
- const el = this.renderer.renderBlock(block, { sessionId }, blocksEl);
1725
- if (el) {
1726
- if (item.seq !== undefined) seenSeqs.add(item.seq);
1727
- blocksEl.appendChild(el);
1728
- }
1729
- } catch (_) {}
1730
- }
1731
- this._bgCache.delete(conversationId);
1732
- this.scrollToBottom();
1733
- }
1734
-
1735
- async _recoverMissedChunks() {
1736
- if (!this.state.currentSession?.id) return;
1737
-
1738
- const sessionId = this.state.currentSession.id;
1739
- const lastSeq = this.wsManager.getLastSeq(sessionId);
1740
-
1741
- try {
1742
- const { chunks: rawChunks } = await window.wsClient.rpc('sess.chunks', { id: sessionId, sinceSeq: lastSeq });
1743
- if (!rawChunks || rawChunks.length === 0) return;
1744
-
1745
- const chunks = rawChunks.map(c => ({
1746
- ...c,
1747
- block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
1748
- })).filter(c => c.block && c.block.type);
1749
-
1750
- const seenSeqs = (this._renderedSeqs || {})[sessionId];
1751
- const dedupedChunks = chunks.filter(c => !seenSeqs || !seenSeqs.has(c.sequence));
1752
-
1753
- if (dedupedChunks.length > 0) {
1754
- for (const chunk of dedupedChunks) {
1755
- try { this.renderChunk(chunk); } catch (chunkErr) { console.warn('Skipping bad chunk:', chunkErr.message); }
1756
- }
1757
- }
1758
- } catch (e) {
1759
- console.warn('Chunk recovery failed:', e.message);
1760
- }
1761
- }
1762
-
1763
- _dedupedFetch(key, fetchFn) {
1764
- if (this._inflightRequests.has(key)) {
1765
- return this._inflightRequests.get(key);
1766
- }
1767
- const promise = fetchFn().finally(() => {
1768
- this._inflightRequests.delete(key);
1769
- });
1770
- this._inflightRequests.set(key, promise);
1771
- return promise;
1772
- }
1773
-
1774
- _insertPlaceholder(sessionId) {
1775
- this._removePlaceholder();
1776
- const streamingEl = document.getElementById(`streaming-${sessionId}`);
1777
- if (!streamingEl) return;
1778
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
1779
- if (!blocksEl) return;
1780
- const ph = document.createElement('div');
1781
- ph.className = 'chunk-placeholder';
1782
- ph.id = 'chunk-placeholder-active';
1783
- blocksEl.appendChild(ph);
1784
- this._placeholderAutoRemove = setTimeout(() => this._removePlaceholder(), 500);
1785
- }
1786
-
1787
- _removePlaceholder() {
1788
- if (this._placeholderAutoRemove) { clearTimeout(this._placeholderAutoRemove); this._placeholderAutoRemove = null; }
1789
- const ph = document.getElementById('chunk-placeholder-active');
1790
- if (ph && ph.parentNode) ph.remove();
1791
- }
1792
-
1793
- _trackBlockHeight(block, element) {
1794
- if (!element || !block?.type) return;
1795
- const h = element.offsetHeight;
1796
- if (h <= 0) return;
1797
- if (!this._blockHeightAvg) this._blockHeightAvg = {};
1798
- const t = block.type;
1799
- if (!this._blockHeightAvg[t]) this._blockHeightAvg[t] = { sum: 0, count: 0 };
1800
- this._blockHeightAvg[t].sum += h;
1801
- this._blockHeightAvg[t].count++;
1802
- }
1803
-
1804
- _estimatedBlockHeight(type) {
1805
- const defaults = { text: 40, tool_use: 60, tool_result: 40 };
1806
- if (this._blockHeightAvg?.[type]?.count >= 3) {
1807
- return this._blockHeightAvg[type].sum / this._blockHeightAvg[type].count;
1808
- }
1809
- return defaults[type] || 40;
1810
- }
1811
-
1812
- _startThinkingCountdown() {
1813
- this._clearThinkingCountdown();
1814
- if (!this._lastSendTime) return;
1815
- const predicted = this.wsManager?.latency?.predicted || 0;
1816
- const estimatedWait = predicted + this._serverProcessingEstimate;
1817
- if (estimatedWait < 1000) return;
1818
- let remaining = Math.ceil(estimatedWait / 1000);
1819
- const update = () => {
1820
- const indicator = document.querySelector('.streaming-indicator');
1821
- if (!indicator) return;
1822
- if (remaining > 0) {
1823
- indicator.textContent = `Thinking... (~${remaining}s)`;
1824
- remaining--;
1825
- this._countdownTimer = setTimeout(update, 1000);
1826
- } else {
1827
- indicator.textContent = 'Thinking... (taking longer than expected)';
1828
- }
1829
- };
1830
- this._countdownTimer = setTimeout(update, 100);
1831
- }
1832
-
1833
- _clearThinkingCountdown() {
1834
- if (this._countdownTimer) { clearTimeout(this._countdownTimer); this._countdownTimer = null; }
1835
- }
1836
-
1837
- _setupDebugHooks() {
1838
- if (typeof window === 'undefined') return;
1839
- const self = this;
1840
- window.__debug = {
1841
- getState: () => ({
1842
- latencyEma: self.wsManager?._latencyEma || null,
1843
- serverProcessingEstimate: self._serverProcessingEstimate,
1844
- latencyTrend: self.wsManager?.latency?.trend || null
1845
- }),
1846
-
1847
- getSyncState: () => ({
1848
- currentConversation: self.state.currentConversation,
1849
- isStreaming: self._convIsStreaming(self.state.currentConversation?.id),
1850
- streamingConversations: Array.from(self.state.streamingConversations),
1851
- convMachineStates: typeof convMachineAPI !== 'undefined' ? Object.fromEntries([...window.__convMachines].map(([k, a]) => [k, a.getSnapshot().value])) : {},
1852
- toolInstallMachineStates: typeof window.__toolInstallMachines !== 'undefined' ? Object.fromEntries([...window.__toolInstallMachines].map(([k, a]) => [k, a.getSnapshot().value])) : {},
1853
- voiceMachineState: typeof window.__voiceMachine !== 'undefined' ? window.__voiceMachine.getSnapshot().value : 'unknown',
1854
- convListMachineState: typeof window.__convListMachine !== 'undefined' ? window.__convListMachine.getSnapshot().value : 'unknown',
1855
- promptMachineState: typeof window.__promptMachine !== 'undefined' ? window.__promptMachine.getSnapshot().value : 'unknown',
1856
- wsConnectionState: self.wsManager?._wsActor?.getSnapshot().value || 'unknown',
1857
- rendererEventQueueLength: self.renderer?.eventQueue?.length || 0,
1858
- rendererEventHistoryLength: self.renderer?.eventHistory?.length || 0,
1859
- }),
1860
-
1861
- getMessageState: () => {
1862
- const output = document.querySelector('.conversation-messages');
1863
- if (!output) return { error: 'No conversation output found' };
1864
- const messageCount = output.querySelectorAll('.message').length;
1865
- const queueItems = output.querySelectorAll('.queue-item').length;
1866
- const pendingMessages = output.querySelectorAll('.message-sending').length;
1867
- return { messageCount, queueItems, pendingMessages };
1868
- }
1869
- };
1870
- }
1871
-
1872
- showLoadingSpinner() {
1873
- document.documentElement.style.pointerEvents = 'auto';
1874
- const indicator = document.querySelector('[data-model-dl-indicator]');
1875
- if (indicator && !indicator.classList.contains('visible')) {
1876
- indicator.classList.add('visible');
1877
- }
1878
- }
1879
-
1880
- hideLoadingSpinner() {
1881
- const indicator = document.querySelector('[data-model-dl-indicator]');
1882
- if (indicator && indicator.classList.contains('visible')) {
1883
- indicator.classList.remove('visible');
1884
- }
1885
- }
1886
-
1887
- _showWelcomeScreen() {
1888
- const outputEl = document.getElementById('output');
1889
- if (!outputEl) return;
1890
- const agents = this.state.agents || [];
1891
- const agentOptions = agents.map(a =>
1892
- `<option value="${this.escapeHtml(a.id)}">${this.escapeHtml(a.name.split(/[\s\-]+/)[0])}</option>`
1893
- ).join('');
1894
- outputEl.innerHTML = `
1895
- <div style="display:flex;align-items:center;justify-content:center;height:100%;flex-direction:column;gap:2rem;padding:2rem;">
1896
- <div style="text-align:center;">
1897
- <h1 style="margin:0;font-size:2.5rem;color:var(--color-text-primary);">Welcome to AgentGUI</h1>
1898
- <p style="margin:1rem 0 0 0;font-size:1.1rem;color:var(--color-text-secondary);">Start a new conversation or select one from the sidebar</p>
1899
- </div>
1900
- ${agents.length > 0 ? `
1901
- <div style="display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
1902
- <label style="font-size:0.85rem;color:var(--color-text-secondary);font-weight:500;">Select Agent</label>
1903
- <select id="welcomeAgentSelect" style="padding:0.5rem 1rem;border-radius:0.375rem;border:1px solid var(--color-border);background:var(--color-bg-secondary);color:var(--color-text-primary);font-size:1rem;cursor:pointer;">
1904
- ${agentOptions}
1905
- </select>
1906
- </div>
1907
- ` : ''}
1908
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1rem;max-width:600px;">
1909
- <div style="padding:1.5rem;border-radius:0.5rem;background:var(--color-bg-secondary);border:1px solid var(--color-border);">
1910
- <h3 style="margin:0 0 0.5rem 0;color:var(--color-primary);">New Conversation</h3>
1911
- <p style="margin:0;font-size:0.9rem;color:var(--color-text-secondary);">Create a new chat with any AI agent</p>
1912
- </div>
1913
- <div style="padding:1.5rem;border-radius:0.5rem;background:var(--color-bg-secondary);border:1px solid var(--color-border);">
1914
- <h3 style="margin:0 0 0.5rem 0;color:var(--color-primary);">Available Agents</h3>
1915
- <p style="margin:0;font-size:0.9rem;color:var(--color-text-secondary);">${agents.length > 0 ? agents.map(a => a.name.split(/[\s\-]+/)[0]).join(', ') : 'Claude Code, Gemini, OpenCode, and more'}</p>
1916
- </div>
1917
- </div>
1918
- </div>
1919
- `;
1920
- const welcomeSel = document.getElementById('welcomeAgentSelect');
1921
- if (welcomeSel) {
1922
- if (this.ui.cliSelector) welcomeSel.value = this.ui.cliSelector.value;
1923
- welcomeSel.addEventListener('change', () => {
1924
- if (this.ui.cliSelector) {
1925
- this.ui.cliSelector.value = welcomeSel.value;
1926
- this.ui.cliSelector.dispatchEvent(new Event('change'));
1927
- }
1928
- });
1929
- }
1930
- }
1931
-
1932
- _showSkeletonLoading(conversationId) {
1933
- const outputEl = document.getElementById('output');
1934
- if (!outputEl) return;
1935
- const conv = this.state.conversations.find(c => c.id === conversationId);
1936
- const title = conv?.title || 'Conversation';
1937
- const wdInfo = conv?.workingDirectory ? `${this.escapeHtml(conv.workingDirectory)}` : '';
1938
- const timestamp = conv ? new Date(conv.created_at).toLocaleDateString() : '';
1939
- const metaParts = [timestamp];
1940
- if (wdInfo) metaParts.push(wdInfo);
1941
- outputEl.innerHTML = `
1942
- <div class="conversation-header">
1943
- <h2>${this.escapeHtml(title)}</h2>
1944
- <p class="text-secondary">${metaParts.join(' - ')}</p>
1945
- </div>
1946
- <div class="conversation-messages">
1947
- <div class="skeleton-loading">
1948
- <div class="skeleton-block skeleton-pulse" style="height:3rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1949
- <div class="skeleton-block skeleton-pulse" style="height:6rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1950
- <div class="skeleton-block skeleton-pulse" style="height:2rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1951
- <div class="skeleton-block skeleton-pulse" style="height:5rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1952
- </div>
1953
- </div>
1954
- `;
1955
- }
1956
-
1957
- async streamToConversation(conversationId, prompt, agentId, model, subAgent) {
1958
- try {
1959
- if (this.wsManager.isConnected) {
1960
- this.wsManager.sendMessage({ type: 'subscribe', conversationId });
1961
- }
1962
-
1963
- let finalPrompt = prompt;
1964
- const streamBody = { id: conversationId, content: finalPrompt, agentId };
1965
- if (model) streamBody.model = model;
1966
- if (subAgent) streamBody.subAgent = subAgent;
1967
- let result;
1968
- try {
1969
- result = await window.wsClient.rpc('msg.stream', streamBody);
1970
- } catch (e) {
1971
- if (e.code === 404) {
1972
- console.warn('Conversation not found, recreating:', conversationId);
1973
- const conv = this.state.currentConversation;
1974
- const createBody = { agentId, title: conv?.title || prompt.substring(0, 50), workingDirectory: conv?.workingDirectory || null };
1975
- if (model) createBody.model = model;
1976
- if (subAgent) createBody.subAgent = subAgent;
1977
- const { conversation: newConv } = await window.wsClient.rpc('conv.new', createBody);
1978
- window.ConversationState?.selectConversation(newConv.id, 'stream_recreate', 1);
1979
- this.state.currentConversation = newConv;
1980
- if (window.conversationManager) {
1981
- window.conversationManager.loadConversations();
1982
- window.conversationManager.select(newConv.id);
1983
- }
1984
- this.updateUrlForConversation(newConv.id);
1985
- return this.streamToConversation(newConv.id, prompt, agentId, model, subAgent);
1986
- }
1987
- throw e;
1988
- }
1989
-
1990
- if (result.queued) {
1991
- this._dbg('Message queued, position:', result.queuePosition);
1992
- this.enableControls();
1993
- return;
1994
- }
1995
-
1996
- if (result.session && this.wsManager.isConnected) {
1997
- this.wsManager.subscribeToSession(result.session.id);
1998
- }
1999
-
2000
- this._lastSendTime = Date.now();
2001
- this.emit('execution:started', result);
2002
- } catch (error) {
2003
- console.error('Stream execution error:', error);
2004
- this.showError('Failed to stream execution: ' + error.message);
2005
- this.enableControls();
2006
- }
2007
- }
2008
-
2009
- _hydrateSessionBlocks(blocksEl, list) {
2010
- const blockFrag = document.createDocumentFragment();
2011
- const deferred = [];
2012
- for (const chunk of list) {
2013
- if (!chunk.block?.type) continue;
2014
- const bt = chunk.block.type;
2015
- if (bt === 'tool_result' || bt === 'tool_status' || bt === 'hook_progress') { deferred.push(chunk); continue; }
2016
- const el = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
2017
- if (!el) continue;
2018
- el.classList.add('block-loaded');
2019
- blockFrag.appendChild(el);
2020
- }
2021
- blocksEl.appendChild(blockFrag);
2022
- const toolUseIndex = new Map();
2023
- blocksEl.querySelectorAll('.block-tool-use[data-tool-use-id]').forEach(el => toolUseIndex.set(el.dataset.toolUseId, el));
2024
- for (const chunk of deferred) {
2025
- const b = chunk.block;
2026
- if (b.type === 'tool_result') {
2027
- const toolUseEl = (b.tool_use_id && toolUseIndex.get(b.tool_use_id))
2028
- || (blocksEl.lastElementChild?.classList.contains('block-tool-use') ? blocksEl.lastElementChild : null);
2029
- if (toolUseEl) this.renderer.mergeResultIntoToolUse(toolUseEl, b);
2030
- } else if (b.type === 'tool_status') {
2031
- const toolUseEl = b.tool_use_id && toolUseIndex.get(b.tool_use_id);
2032
- if (toolUseEl) {
2033
- const isError = b.status === 'failed';
2034
- const isDone = b.status === 'completed';
2035
- if (isDone || isError) toolUseEl.classList.add(isError ? 'tool-result-error' : 'tool-result-success');
2036
- }
2037
- }
2038
- }
2039
- }
2040
-
2041
- _getLazyObserver() {
2042
- if (this._lazyObserver) return this._lazyObserver;
2043
- if (typeof IntersectionObserver === 'undefined') return null;
2044
- this._lazyObserver = new IntersectionObserver((entries) => {
2045
- for (const entry of entries) {
2046
- if (!entry.isIntersecting) continue;
2047
- const msgDiv = entry.target;
2048
- const pendingChunks = msgDiv._lazyChunks;
2049
- if (!pendingChunks) continue;
2050
- delete msgDiv._lazyChunks;
2051
- this._lazyObserver.unobserve(msgDiv);
2052
- const blocksEl = msgDiv.querySelector('.message-blocks');
2053
- if (blocksEl) this._hydrateSessionBlocks(blocksEl, pendingChunks);
2054
- }
2055
- }, { rootMargin: '400px 0px' });
2056
- return this._lazyObserver;
2057
- }
2058
-
2059
- _renderConversationContent(messagesContainer, chunks, userMessages, activeSessionId) {
2060
- if (!chunks || chunks.length === 0) return;
2061
- const sessionMap = new Map();
2062
- for (const chunk of chunks) {
2063
- if (!sessionMap.has(chunk.sessionId)) sessionMap.set(chunk.sessionId, []);
2064
- sessionMap.get(chunk.sessionId).push(chunk);
2065
- }
2066
-
2067
- const sessionIds = [...sessionMap.keys()];
2068
- const EAGER_TAIL = 8;
2069
- const eagerSet = new Set(sessionIds.slice(-EAGER_TAIL));
2070
- if (activeSessionId) eagerSet.add(activeSessionId);
2071
- const observer = sessionIds.length > EAGER_TAIL ? this._getLazyObserver() : null;
2072
-
2073
- const frag = document.createDocumentFragment();
2074
- let ui = 0;
2075
- for (const [sid, list] of sessionMap) {
2076
- const sessionStart = list[0].created_at;
2077
- while (ui < userMessages.length && userMessages[ui].created_at <= sessionStart) {
2078
- const m = userMessages[ui++];
2079
- const uDiv = document.createElement('div');
2080
- uDiv.className = 'message message-user';
2081
- uDiv.setAttribute('data-msg-id', m.id);
2082
- uDiv.innerHTML = `<div class="message-role">User<button class="msg-edit-btn" data-edit-msg="${this.escapeHtml(m.id)}" title="Edit and re-run">&#9998;</button></div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
2083
- frag.appendChild(uDiv);
2084
- }
2085
- const isActive = sid === activeSessionId;
2086
- const msgDiv = document.createElement('div');
2087
- msgDiv.className = `message message-assistant${isActive ? ' streaming-message' : ''}`;
2088
- msgDiv.id = isActive ? `streaming-${sid}` : `message-${sid}`;
2089
- msgDiv.setAttribute('data-session-id', sid);
2090
- msgDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
2091
- const blocksEl = msgDiv.querySelector('.message-blocks');
2092
-
2093
- if (observer && !eagerSet.has(sid)) {
2094
- msgDiv._lazyChunks = list;
2095
- observer.observe(msgDiv);
2096
- } else {
2097
- this._hydrateSessionBlocks(blocksEl, list);
2098
- }
2099
-
2100
- if (isActive) {
2101
- const ind = document.createElement('div');
2102
- ind.className = 'streaming-indicator';
2103
- ind.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
2104
- ind.innerHTML = '<span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span><span class="streaming-indicator-label">Processing...</span>';
2105
- msgDiv.appendChild(ind);
2106
- } else {
2107
- const ts = document.createElement('div');
2108
- ts.className = 'message-timestamp';
2109
- ts.textContent = new Date(list[list.length - 1].created_at).toLocaleString();
2110
- msgDiv.appendChild(ts);
2111
- }
2112
- frag.appendChild(msgDiv);
2113
- }
2114
- while (ui < userMessages.length) {
2115
- const m = userMessages[ui++];
2116
- const uDiv = document.createElement('div');
2117
- uDiv.className = 'message message-user';
2118
- uDiv.setAttribute('data-msg-id', m.id);
2119
- uDiv.innerHTML = `<div class="message-role">User<button class="msg-edit-btn" data-edit-msg="${this.escapeHtml(m.id)}" title="Edit and re-run">&#9998;</button></div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
2120
- frag.appendChild(uDiv);
2121
- }
2122
- messagesContainer.appendChild(frag);
2123
- }
2124
-
2125
- renderChunk(chunk) {
2126
- try { return this._renderChunkInner(chunk); } catch (e) { console.error('[render-error] chunk:', e); }
2127
- }
2128
- _renderChunkInner(chunk) {
2129
- if (!chunk || !chunk.block) return;
2130
- const seq = chunk.sequence;
2131
- if (seq !== undefined) {
2132
- const seen = (this._renderedSeqs = this._renderedSeqs || {})[chunk.sessionId] || (this._renderedSeqs[chunk.sessionId] = new Set());
2133
- if (seen.has(seq)) return;
2134
- seen.add(seq);
2135
- }
2136
- const streamingEl = document.getElementById(`streaming-${chunk.sessionId}`);
2137
- if (!streamingEl) return;
2138
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
2139
- if (!blocksEl) return;
2140
- if (chunk.block.type === 'tool_result') {
2141
- const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
2142
- const lastEl = blocksEl.lastElementChild;
2143
- const toolUseEl = matchById || (lastEl?.classList?.contains('block-tool-use') ? lastEl : null);
2144
- if (toolUseEl) {
2145
- this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
2146
- this.scrollToBottom();
2147
- return;
2148
- }
2149
- }
2150
- if (chunk.block.type === 'tool_status' || chunk.block.type === 'hook_progress') return;
2151
- const element = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
2152
- if (!element) { this.scrollToBottom(); return; }
2153
- blocksEl.appendChild(element);
2154
- this.scrollToBottom();
2155
- }
2156
-
2157
- async loadAgents() {
2158
- return this._dedupedFetch('loadAgents', async () => {
2159
- try {
2160
- const { agents } = await window.wsClient.rpc('agent.ls');
2161
- this.state.agents = agents;
2162
-
2163
- const displayAgents = agents;
2164
-
2165
- if (this.ui.cliSelector) {
2166
- if (displayAgents.length > 0) {
2167
- this.ui.cliSelector.innerHTML = displayAgents
2168
- .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
2169
- .join('');
2170
- this.ui.cliSelector.style.display = 'inline-block';
2171
- } else {
2172
- this.ui.cliSelector.innerHTML = '';
2173
- this.ui.cliSelector.style.display = 'none';
2174
- }
2175
- }
2176
-
2177
- window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
2178
-
2179
- if (displayAgents.length > 0 && !this._agentLocked) {
2180
- const firstId = displayAgents[0].id;
2181
- await this.loadSubAgentsForCli(firstId);
2182
- this.loadModelsForAgent(this.getEffectiveAgentId());
2183
- }
2184
- return agents;
2185
- } catch (error) {
2186
- console.error('Failed to load agents:', error);
2187
- return [];
2188
- }
2189
- });
2190
- }
2191
-
2192
- async loadSubAgentsForCli(cliAgentId) {
2193
- if (this.ui.agentSelector) {
2194
- this.ui.agentSelector.innerHTML = '';
2195
- this.ui.agentSelector.style.display = 'none';
2196
- }
2197
- try {
2198
- const { subAgents } = await window.wsClient.rpc('agent.subagents', { id: cliAgentId });
2199
- if (subAgents && subAgents.length > 0 && this.ui.agentSelector) {
2200
- this.ui.agentSelector.innerHTML = subAgents
2201
- .map(a => `<option value="${a.id}">${a.name.split(/[\s\-]+/)[0]}</option>`)
2202
- .join('');
2203
- this.ui.agentSelector.style.display = 'inline-block';
2204
- this._dbg(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
2205
- const firstSubAgentId = subAgents[0].id;
2206
- this.ui.agentSelector.value = firstSubAgentId;
2207
- this.loadModelsForAgent(cliAgentId); // models keyed to parent agent
2208
- } else {
2209
- this._dbg(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
2210
- const cliToAcpMap = {
2211
- 'cli-opencode': 'opencode',
2212
- 'cli-gemini': 'gemini',
2213
- 'cli-kilo': 'kilo',
2214
- 'cli-codex': 'codex'
2215
- };
2216
- const acpAgentId = cliToAcpMap[cliAgentId] || cliAgentId;
2217
- this.loadModelsForAgent(acpAgentId);
2218
- }
2219
- } catch (err) {
2220
- console.warn(`[Agent Selector] Failed to load sub-agents for ${cliAgentId}:`, err.message);
2221
- const cliToAcpMap = {
2222
- 'cli-opencode': 'opencode',
2223
- 'cli-gemini': 'gemini',
2224
- 'cli-kilo': 'kilo',
2225
- 'cli-codex': 'codex'
2226
- };
2227
- const acpAgentId = cliToAcpMap[cliAgentId] || cliAgentId;
2228
- this.loadModelsForAgent(acpAgentId);
2229
- }
2230
- }
2231
-
2232
- async checkSpeechStatus() {
2233
- try {
2234
- const status = await window.wsClient.rpc('speech.status');
2235
- if (status.modelsComplete) {
2236
- this._modelDownloadProgress = { done: true, complete: true };
2237
- this._modelDownloadInProgress = false;
2238
- } else if (status.modelsDownloading) {
2239
- this._modelDownloadProgress = status.modelsProgress || { downloading: true };
2240
- this._modelDownloadInProgress = true;
2241
- } else {
2242
- this._modelDownloadProgress = { done: false };
2243
- this._modelDownloadInProgress = false;
2244
- }
2245
- } catch (error) {
2246
- console.error('Failed to check speech status:', error);
2247
- this._modelDownloadProgress = { done: false };
2248
- this._modelDownloadInProgress = false;
2249
- }
2250
- }
2251
-
2252
- async loadModelsForAgent(agentId) {
2253
- if (!agentId || !this.ui.modelSelector) return;
2254
- const cached = this._modelCache.get(agentId);
2255
- if (cached) {
2256
- this._populateModelSelector(cached);
2257
- return;
2258
- }
2259
- try {
2260
- const { models } = await window.wsClient.rpc('agent.models', { id: agentId });
2261
- this._modelCache.set(agentId, models);
2262
- this._populateModelSelector(models);
2263
- } catch (error) {
2264
- console.error('Failed to load models:', error);
2265
- this._populateModelSelector([]);
2266
- }
2267
- }
2268
-
2269
- _populateModelSelector(models) {
2270
- if (!this.ui.modelSelector) return;
2271
- if (!models || models.length === 0) {
2272
- this.ui.modelSelector.innerHTML = '';
2273
- this.ui.modelSelector.setAttribute('data-empty', 'true');
2274
- return;
2275
- }
2276
- this.ui.modelSelector.removeAttribute('data-empty');
2277
- this.ui.modelSelector.innerHTML = models
2278
- .map(m => `<option value="${m.id}">${this.escapeHtml(m.label)}</option>`)
2279
- .join('');
2280
- }
2281
-
2282
- lockAgentAndModel(agentId, model) {
2283
- this._agentLocked = true;
2284
- if (this.ui.cliSelector) {
2285
- this.ui.cliSelector.disabled = true;
2286
- }
2287
-
2288
- this.loadModelsForAgent(agentId).then(() => {
2289
- if (this.ui.modelSelector && model) {
2290
- this.ui.modelSelector.value = model;
2291
- }
2292
- });
2293
- }
2294
-
2295
- unlockAgentAndModel() {
2296
- this._agentLocked = false;
2297
- if (this.ui.cliSelector) {
2298
- this.ui.cliSelector.disabled = false;
2299
- if (this.ui.cliSelector.options.length > 0) {
2300
- this.ui.cliSelector.style.display = 'inline-block';
2301
- }
2302
- }
2303
- if (this.ui.modelSelector) {
2304
- this.ui.modelSelector.disabled = false;
2305
- }
2306
- }
2307
-
2308
- applyAgentAndModelSelection(conversation, hasActivity) {
2309
- const agentId = conversation.agentId || conversation.agentType || null;
2310
- const model = conversation.model || null;
2311
- const subAgent = conversation.subAgent || null;
2312
-
2313
- if (hasActivity) {
2314
- this._setCliSelectorToAgent(agentId);
2315
- this.lockAgentAndModel(agentId, model);
2316
- } else {
2317
- this.unlockAgentAndModel();
2318
- this._setCliSelectorToAgent(agentId);
2319
-
2320
- const effectiveAgentId = agentId || this.getEffectiveAgentId();
2321
- this.loadSubAgentsForCli(effectiveAgentId).then(() => {
2322
- if (subAgent && this.ui.agentSelector) {
2323
- this.ui.agentSelector.value = subAgent;
2324
- }
2325
- });
2326
- this.loadModelsForAgent(effectiveAgentId).then(() => {
2327
- if (model && this.ui.modelSelector) {
2328
- this.ui.modelSelector.value = model;
2329
- }
2330
- });
2331
- }
2332
- }
2333
-
2334
- _setCliSelectorToAgent(agentId) {
2335
- if (this.ui.cliSelector) {
2336
- this.ui.cliSelector.value = agentId;
2337
- if (!this.ui.cliSelector.value) {
2338
- this.ui.cliSelector.selectedIndex = 0;
2339
- }
2340
- }
2341
- }
2342
-
2343
- async loadConversations() {
2344
- const now = Date.now();
2345
- if (this.conversationListCache.data.length > 0 &&
2346
- (now - this.conversationListCache.timestamp) < this.conversationListCache.ttl) {
2347
- this.state.conversations = this.conversationListCache.data;
2348
- return this.conversationListCache.data;
2349
- }
2350
-
2351
- return this._dedupedFetch('loadConversations', async () => {
2352
- try {
2353
- const { conversations } = await window.wsClient.rpc('conv.ls');
2354
- this.state.conversations = conversations;
2355
- this.conversationListCache.data = conversations;
2356
- this.conversationListCache.timestamp = Date.now();
2357
- return conversations;
2358
- } catch (error) {
2359
- console.error('Failed to load conversations:', error);
2360
- return [];
2361
- }
2362
- });
2363
- }
2364
-
2365
- updateConnectionStatus(status) {
2366
- if (this.ui.statusIndicator) {
2367
- this.ui.statusIndicator.dataset.status = status;
2368
- this.ui.statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
2369
- }
2370
- if (status === 'disconnected' || status === 'reconnecting') {
2371
- this._updateConnectionIndicator(status);
2372
- } else if (status === 'connected') {
2373
- this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2374
- }
2375
- }
2376
-
2377
- _updateConnectionIndicator(quality) {
2378
- if (this._indicatorDebounce && !this._modelDownloadInProgress) return;
2379
- this._indicatorDebounce = true;
2380
- setTimeout(() => { this._indicatorDebounce = false; }, 1000);
2381
-
2382
- let indicator = document.getElementById('connection-indicator');
2383
- if (!indicator) {
2384
- indicator = document.createElement('div');
2385
- indicator.id = 'connection-indicator';
2386
- indicator.className = 'connection-indicator';
2387
- indicator.innerHTML = '<span class="connection-dot"></span><span class="connection-label"></span>';
2388
- indicator.addEventListener('click', () => this._toggleConnectionTooltip());
2389
- const header = document.querySelector('.header-right') || document.querySelector('.app-header');
2390
- if (header) {
2391
- header.style.position = 'relative';
2392
- header.appendChild(indicator);
2393
- }
2394
- }
2395
-
2396
- const dot = indicator.querySelector('.connection-dot');
2397
- const label = indicator.querySelector('.connection-label');
2398
- if (!dot || !label) return;
2399
-
2400
- if (this._modelDownloadInProgress) {
2401
- dot.className = 'connection-dot downloading';
2402
- const progress = this._modelDownloadProgress;
2403
- if (progress && progress.totalBytes > 0) {
2404
- const pct = Math.round((progress.totalDownloaded / progress.totalBytes) * 100);
2405
- label.textContent = `Models ${pct}%`;
2406
- } else if (progress && progress.downloading) {
2407
- label.textContent = 'Downloading...';
2408
- } else {
2409
- label.textContent = 'Loading models...';
2410
- }
2411
- return;
2412
- }
2413
-
2414
- dot.className = 'connection-dot';
2415
- if (quality === 'disconnected' || quality === 'reconnecting') {
2416
- dot.classList.add(quality);
2417
- label.textContent = quality === 'reconnecting' ? 'Reconnecting...' : 'Disconnected';
2418
- } else {
2419
- dot.classList.add(quality);
2420
- const latency = this.wsManager?.latency;
2421
- label.textContent = latency?.avg > 0 ? Math.round(latency.avg) + 'ms' : '';
2422
- }
2423
- }
2424
-
2425
- _handleModelDownloadProgress(progress) {
2426
- this._modelDownloadProgress = progress;
2427
- if (progress.status === 'failed' || progress.error) {
2428
- this._modelDownloadInProgress = false;
2429
- console.error('[Models] Download error:', progress.error || progress.status);
2430
- this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2431
- return;
2432
- }
2433
- if (progress.done || progress.status === 'completed') {
2434
- this._modelDownloadInProgress = false;
2435
- this._dbg('[Models] Download complete');
2436
- this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2437
- return;
2438
- }
2439
- if (progress.started || progress.downloading || progress.status === 'downloading' || progress.status === 'connecting') {
2440
- this._modelDownloadInProgress = true;
2441
- this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2442
- }
2443
- }
2444
-
2445
- _handleTTSSetupProgress(data) {
2446
- if (data.step && data.status) {
2447
- this._dbg('[TTS Setup]', data.step, ':', data.status, data.message || '');
2448
- }
2449
- }
2450
-
2451
- _toggleConnectionTooltip() {
2452
- let tooltip = document.getElementById('connection-tooltip');
2453
- if (tooltip) { tooltip.remove(); return; }
2454
-
2455
- const indicator = document.getElementById('connection-indicator');
2456
- if (!indicator) return;
2457
-
2458
- tooltip = document.createElement('div');
2459
- tooltip.id = 'connection-tooltip';
2460
- tooltip.className = 'connection-tooltip';
2461
-
2462
- const latency = this.wsManager?.latency || {};
2463
- const stats = this.wsManager?.stats || {};
2464
- const state = this.wsManager?.connectionState || 'unknown';
2465
-
2466
- tooltip.innerHTML = [
2467
- `<div>State: ${state}</div>`,
2468
- `<div>Latency: ${Math.round(latency.avg || 0)}ms</div>`,
2469
- `<div>Predicted: ${Math.round(latency.predicted || 0)}ms (Kalman)</div>`,
2470
- `<div>Trend: ${latency.trend || 'unknown'}</div>`,
2471
- `<div>Jitter: ${Math.round(latency.jitter || 0)}ms</div>`,
2472
- `<div>Quality: ${latency.quality || 'unknown'}</div>`,
2473
- `<div>Reconnects: ${stats.totalReconnects || 0}</div>`,
2474
- `<div>Uptime: ${stats.lastConnectedTime ? Math.round((Date.now() - stats.lastConnectedTime) / 1000) + 's' : 'N/A'}</div>`
2475
- ].join('');
2476
-
2477
- indicator.appendChild(tooltip);
2478
- setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 5000);
2479
- }
2480
-
2481
- updateMetrics(metrics) {
2482
- const metricsDisplay = document.querySelector('[data-metrics]');
2483
- if (metricsDisplay && metrics) {
2484
- metricsDisplay.textContent = `Batches: ${metrics.totalBatches} | Events: ${metrics.totalEvents} | Avg render: ${metrics.avgRenderTime.toFixed(2)}ms`;
2485
- }
2486
- }
2487
-
2488
- disableControls() {
2489
- if (this.ui.sendButton) this.ui.sendButton.disabled = true;
2490
- if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'DISABLED' });
2491
- }
2492
-
2493
- enableControls() {
2494
- if (this.ui.sendButton) {
2495
- this.ui.sendButton.disabled = !this.wsManager?.isConnected;
2496
- }
2497
- if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
2498
- this.updateBusyPromptArea(this.state.currentConversation?.id);
2499
- }
2500
-
2501
- toggleTheme() {
2502
- const isDark = document.documentElement.classList.toggle('dark');
2503
- localStorage.setItem('theme', isDark ? 'dark' : 'light');
2504
- }
2505
-
2506
- async createNewConversation(workingDirectory, title) {
2507
- try {
2508
- const agentId = this.getEffectiveAgentId();
2509
- const model = this.ui.modelSelector?.value || null;
2510
- const convTitle = title || 'New Conversation';
2511
- const body = { agentId, title: convTitle };
2512
- if (workingDirectory) body.workingDirectory = workingDirectory;
2513
- if (model) body.model = model;
2514
-
2515
- const { conversation } = await window.wsClient.rpc('conv.new', body);
2516
-
2517
- await this.loadConversations();
2518
-
2519
- if (window.conversationManager) {
2520
- await window.conversationManager.loadConversations();
2521
- window.conversationManager.select(conversation.id);
2522
- }
2523
-
2524
- if (this.ui.messageInput) {
2525
- this.ui.messageInput.value = '';
2526
- this.ui.messageInput.focus();
2527
- }
2528
- } catch (error) {
2529
- console.error('Failed to create new conversation:', error);
2530
- this.showError(`Failed to create conversation: ${error.message}`);
2531
- }
2532
- }
2533
-
2534
- cacheCurrentConversation() {
2535
- const convId = this.state.currentConversation?.id;
2536
- if (!convId) return;
2537
- const outputEl = document.getElementById('output');
2538
- if (!outputEl || !outputEl.firstChild) return;
2539
- if (this._convIsStreaming(convId)) return;
2540
-
2541
- this.saveScrollPosition(convId);
2542
- const clone = outputEl.cloneNode(true);
2543
- this.conversationCache.set(convId, {
2544
- dom: clone,
2545
- conversation: this.state.currentConversation,
2546
- timestamp: Date.now()
2547
- });
2548
-
2549
- if (this.conversationCache.size > this.MAX_CACHE_SIZE) {
2550
- const oldest = this.conversationCache.keys().next().value;
2551
- this.conversationCache.delete(oldest);
2552
- }
2553
- }
2554
-
2555
- invalidateCache(conversationId) {
2556
- this.conversationCache.delete(conversationId);
2557
- }
2558
-
2559
- _makeLoadRequest(conversationId) {
2560
- const requestId = ++this._currentRequestId;
2561
- const abortController = new AbortController();
2562
-
2563
- if (this._loadInProgress[conversationId]) {
2564
- const prevReq = this._loadInProgress[conversationId];
2565
- try {
2566
- prevReq.abortController.abort();
2567
- } catch (e) {}
2568
- }
2569
-
2570
- this._loadInProgress[conversationId] = {
2571
- requestId,
2572
- abortController,
2573
- timestamp: Date.now(),
2574
- prevConversationId: this.state.currentConversation?.id
2575
- };
2576
-
2577
- return { requestId, abortController: abortController.signal };
2578
- }
2579
-
2580
- _verifyRequestId(conversationId, requestId) {
2581
- const current = this._loadInProgress[conversationId];
2582
- if (!current) return false;
2583
- if (current.requestId !== requestId) return false;
2584
- return true;
2585
- }
2586
-
2587
- _completeLoadRequest(conversationId, requestId) {
2588
- const req = this._loadInProgress[conversationId];
2589
- if (req && req.requestId === requestId) {
2590
- delete this._loadInProgress[conversationId];
2591
- }
2592
- }
2593
-
2594
- async loadConversationMessages(conversationId) {
2595
- performance.mark(`conv-load-start:${conversationId}`);
2596
- try {
2597
- if (this._previousConvAbort) {
2598
- this._previousConvAbort.abort();
2599
- }
2600
- this._previousConvAbort = new AbortController();
2601
- const convSignal = this._previousConvAbort.signal;
2602
-
2603
- const prevConversationId = this.state.currentConversation?.id;
2604
- const availableFallback = this.state.conversations?.find(c => c.id !== conversationId) || null;
2605
- this.cacheCurrentConversation();
2606
-
2607
- this.removeScrollUpDetection();
2608
- if (this.renderer.resetScrollState) this.renderer.resetScrollState();
2609
- this._userScrolledUp = false;
2610
- this._removeNewContentPill();
2611
-
2612
- if (this.ui.messageInput) {
2613
- this.ui.messageInput.value = '';
2614
- this.ui.messageInput.style.height = 'auto';
2615
- }
2616
-
2617
- if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
2618
- if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
2619
- if (this.ui.queueButton) this.ui.queueButton.classList.remove('visible');
2620
- if (this.ui.sendButton) this.ui.sendButton.style.display = '';
2621
-
2622
- var prevId = this.state.currentConversation?.id;
2623
- if (prevId && prevId !== conversationId) {
2624
- if (this.wsManager.isConnected && !this._convIsStreaming(prevId)) {
2625
- this.wsManager.sendMessage({ type: 'unsubscribe', conversationId: prevId });
2626
- }
2627
- this.state.currentSession = null;
2628
- }
2629
-
2630
- const cachedConv = this.state.conversations.find(c => c.id === conversationId);
2631
- if (cachedConv && this.state.currentConversation?.id !== conversationId) {
2632
- window.ConversationState?.selectConversation(conversationId, 'cache_load', 1);
2633
- this.state.currentConversation = cachedConv;
2634
- }
2635
-
2636
- this.updateUrlForConversation(conversationId);
2637
- if (this.wsManager.isConnected) {
2638
- this.wsManager.sendMessage({ type: 'subscribe', conversationId });
2639
- }
2640
-
2641
- const cached = this.conversationCache.get(conversationId);
2642
- if (cached) { this.conversationCache.delete(conversationId); this.conversationCache.set(conversationId, cached); }
2643
- if (cached && (Date.now() - cached.timestamp) < 600000) {
2644
- const outputEl = document.getElementById('output');
2645
- if (outputEl) {
2646
- const children = [];
2647
- while (cached.dom.firstChild) children.push(cached.dom.firstChild);
2648
- outputEl.replaceChildren(...children);
2649
- window.ConversationState?.selectConversation(conversationId, 'dom_cache_load', 1);
2650
- this.state.currentConversation = cached.conversation;
2651
- window.dispatchEvent(new CustomEvent('conversation-changed', { detail: { conversationId, conversation: cached.conversation } }));
2652
- const cachedHasActivity = cached.conversation.messageCount > 0 || this._convIsStreaming(conversationId);
2653
- this.applyAgentAndModelSelection(cached.conversation, cachedHasActivity);
2654
- this.conversationCache.delete(conversationId);
2655
- if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
2656
- this._flushBgCache && this._flushBgCache(conversationId);
2657
- this.setupScrollUpDetection && this.setupScrollUpDetection();
2658
- this.syncPromptState(conversationId);
2659
- this.restoreScrollPosition(conversationId);
2660
- return;
2661
- }
2662
- }
2663
-
2664
- this.conversationCache.delete(conversationId);
2665
-
2666
- if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
2667
- this._showSkeletonLoading(conversationId);
2668
- await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
2669
-
2670
- let fullData;
2671
- try {
2672
- fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
2673
- performance.mark(`conv-data-received:${conversationId}`);
2674
- if (convSignal.aborted) return;
2675
- } catch (wsErr) {
2676
- if (wsErr.code === 404) {
2677
- console.warn('Conversation no longer exists:', conversationId);
2678
- window.ConversationState?.clear('conversation_not_found');
2679
- this.state.currentConversation = null;
2680
- if (window.conversationManager) window.conversationManager.loadConversations();
2681
- const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
2682
- if (fallbackConv && fallbackConv !== conversationId) {
2683
- this._dbg('Resuming from fallback conversation:', fallbackConv);
2684
- this.showError('Conversation not found. Resuming previous conversation.');
2685
- await this.loadConversationMessages(fallbackConv);
2686
- } else {
2687
- const outputEl = document.getElementById('output');
2688
- if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
2689
- this.enableControls();
2690
- }
2691
- return;
2692
- }
2693
- try {
2694
- const base = window.__BASE_URL || '';
2695
- const [convRes, chunksRes, msgsRes] = await Promise.all([
2696
- fetch(`${base}/api/conversations/${conversationId}`),
2697
- fetch(`${base}/api/conversations/${conversationId}/chunks`),
2698
- fetch(`${base}/api/conversations/${conversationId}/messages?limit=500`)
2699
- ]);
2700
- const convData = await convRes.json();
2701
- const chunksData = await chunksRes.json();
2702
- const msgsData = await msgsRes.json();
2703
- fullData = {
2704
- conversation: convData.conversation,
2705
- isActivelyStreaming: false,
2706
- latestSession: null,
2707
- chunks: chunksData.chunks || [],
2708
- totalChunks: chunksData.totalChunks || (chunksData.chunks || []).length,
2709
- messages: msgsData.messages || []
2710
- };
2711
- if (convSignal.aborted) return;
2712
- } catch (restErr) {
2713
- throw wsErr;
2714
- }
2715
- }
2716
- if (convSignal.aborted) return;
2717
- const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
2718
-
2719
- window.ConversationState?.selectConversation(conversationId, 'server_load', 1);
2720
- this.state.currentConversation = conversation;
2721
- window.dispatchEvent(new CustomEvent('conversation-changed', { detail: { conversationId, conversation } }));
2722
- const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this._convIsStreaming(conversationId);
2723
- this.applyAgentAndModelSelection(conversation, hasActivity);
2724
-
2725
- const queuePromise = window.wsClient.rpc('q.ls', { id: conversationId }).catch(() => ({ queue: [] }));
2726
- const chunks = (rawChunks || []).map(chunk => ({
2727
- ...chunk,
2728
- block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
2729
- }));
2730
-
2731
- const { queue: queueResult } = await queuePromise;
2732
- const queuedMessageIds = new Set((queueResult || []).map(q => q.messageId));
2733
- const userMessages = (allMessages || []).filter(m => m.role === 'user' && !queuedMessageIds.has(m.id));
2734
- const hasMoreChunks = totalChunks && chunks.length < totalChunks;
2735
-
2736
- const clientKnowsStreaming = this._convIsStreaming(conversationId);
2737
- const shouldResumeStreaming = latestSession &&
2738
- (latestSession.status === 'active' || latestSession.status === 'pending');
2739
-
2740
- if (shouldResumeStreaming) {
2741
- this._setConvStreaming(conversationId, true, latestSession?.id, null);
2742
- window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_start', conversationId, sessionId: latestSession?.id } }));
2743
- } else {
2744
- this._setConvStreaming(conversationId, false);
2745
- window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_complete', conversationId } }));
2746
- }
2747
-
2748
- if (this.ui.messageInput) {
2749
- this.ui.messageInput.disabled = false;
2750
- }
2751
-
2752
- const outputEl = document.getElementById('output');
2753
- if (outputEl) {
2754
- const wdInfo = conversation.workingDirectory ? `${this.escapeHtml(conversation.workingDirectory)}` : '';
2755
- const timestamp = new Date(conversation.created_at).toLocaleDateString();
2756
- const metaParts = [timestamp];
2757
- if (wdInfo) metaParts.push(wdInfo);
2758
- outputEl.innerHTML = `
2759
- <div class="conversation-header">
2760
- <h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
2761
- <p class="text-secondary">${metaParts.join(' - ')}</p>
2762
- </div>
2763
- <div class="conversation-messages"></div>
2764
- `;
2765
-
2766
- const messagesEl = outputEl.querySelector('.conversation-messages');
2767
-
2768
- if (hasMoreChunks) {
2769
- const loadMoreBtn = document.createElement('button');
2770
- loadMoreBtn.className = 'btn btn-secondary';
2771
- loadMoreBtn.style.cssText = 'width:100%;margin-bottom:1rem;padding:0.5rem;font-size:0.8rem;';
2772
- loadMoreBtn.textContent = `Load earlier messages (${totalChunks - chunks.length} more chunks)`;
2773
- loadMoreBtn.addEventListener('click', async () => {
2774
- loadMoreBtn.disabled = true;
2775
- loadMoreBtn.textContent = 'Loading...';
2776
- try {
2777
- try {
2778
- await window.wsClient.rpc('conv.full', { id: conversationId, allChunks: true });
2779
- } catch (wsErr) {}
2780
- this.invalidateCache(conversationId);
2781
- await this.loadConversationMessages(conversationId);
2782
- } catch (e) {
2783
- loadMoreBtn.textContent = 'Failed to load. Try again.';
2784
- loadMoreBtn.disabled = false;
2785
- }
2786
- });
2787
- messagesEl.appendChild(loadMoreBtn);
2788
- }
2789
-
2790
- if (chunks.length > 0) {
2791
- const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
2792
- performance.mark(`conv-render-start:${conversationId}`);
2793
- await new Promise(r => requestAnimationFrame(r));
2794
- if (!convSignal.aborted) this._renderConversationContent(messagesEl, chunks, userMessages, activeSessionId);
2795
- performance.mark(`conv-render-complete:${conversationId}`);
2796
- performance.measure(`conv-render:${conversationId}`, `conv-render-start:${conversationId}`, `conv-render-complete:${conversationId}`);
2797
- performance.measure(`conv-data-fetch:${conversationId}`, `conv-load-start:${conversationId}`, `conv-data-received:${conversationId}`);
2798
- } else {
2799
- if (!convSignal.aborted) messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
2800
- }
2801
-
2802
- if (!convSignal.aborted && shouldResumeStreaming && latestSession && chunks.length === 0) {
2803
- const streamDiv = document.createElement('div');
2804
- streamDiv.id = `streaming-${latestSession.id}`;
2805
- streamDiv.className = 'streaming-message';
2806
- const indicatorDiv = document.createElement('div');
2807
- indicatorDiv.className = 'streaming-indicator';
2808
- indicatorDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
2809
- indicatorDiv.innerHTML = `
2810
- <span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
2811
- <span class="streaming-indicator-label">Agent is starting...</span>
2812
- `;
2813
- streamDiv.appendChild(indicatorDiv);
2814
- messagesEl.appendChild(streamDiv);
2815
- }
2816
-
2817
- if (shouldResumeStreaming && latestSession) {
2818
- this._setConvStreaming(conversationId, true, latestSession.id, null);
2819
- this.state.currentSession = {
2820
- id: latestSession.id,
2821
- conversationId: conversationId,
2822
- agentId: conversation.agentType || null,
2823
- startTime: latestSession.created_at
2824
- };
2825
-
2826
- if (this.wsManager.isConnected) {
2827
- this.wsManager.subscribeToSession(latestSession.id);
2828
- this.wsManager.sendMessage({ type: 'subscribe', conversationId });
2829
- }
2830
-
2831
- this.updateUrlForConversation(conversationId, latestSession.id);
2832
-
2833
- this._flushBgCache(conversationId, latestSession.id);
2834
-
2835
- this.syncPromptState(conversationId);
2836
- } else {
2837
- this.syncPromptState(conversationId);
2838
- }
2839
-
2840
- if (this.ui.sendButton) {
2841
- this.ui.sendButton.disabled = false;
2842
- }
2843
-
2844
- this.restoreScrollPosition(conversationId);
2845
- this.setupScrollUpDetection(conversationId);
2846
-
2847
- this.fetchAndRenderQueue(conversationId);
2848
-
2849
- }
2850
- } catch (error) {
2851
- if (error.name === 'AbortError') return;
2852
- console.error('Failed to load conversation messages:', error);
2853
- const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
2854
- if (fallbackConv && fallbackConv !== conversationId) {
2855
- this._dbg('Resuming from fallback conversation due to error:', fallbackConv);
2856
- this.showError('Failed to load conversation. Resuming previous conversation.');
2857
- try {
2858
- await this.loadConversationMessages(fallbackConv);
2859
- } catch (fallbackError) {
2860
- console.error('Failed to resume fallback conversation:', fallbackError);
2861
- this.showError('Failed to load conversation: ' + error.message);
2862
- }
2863
- } else {
2864
- this.showError('Failed to load conversation: ' + error.message);
2865
- }
2866
- }
2867
- }
2868
-
2869
- syncPromptState(conversationId) {
2870
- const conversation = this.state.currentConversation;
2871
- if (!conversation || conversation.id !== conversationId) return;
2872
-
2873
- if (this.ui.messageInput) {
2874
- this.ui.messageInput.disabled = false;
2875
- }
2876
-
2877
- this.updateBusyPromptArea(conversationId);
2878
- }
2879
-
2880
- updateBusyPromptArea(conversationId) {
2881
- if (this.state.currentConversation?.id !== conversationId) return;
2882
- const isStreaming = this._convIsStreaming(conversationId);
2883
- const isConnected = this.wsManager?.isConnected;
2884
-
2885
- const injectBtn = document.getElementById('injectBtn');
2886
- const queueBtn = document.getElementById('queueBtn');
2887
- const stopBtn = document.getElementById('stopBtn');
2888
-
2889
- [injectBtn, queueBtn, stopBtn].forEach(btn => {
2890
- if (!btn) return;
2891
- btn.classList.toggle('visible', isStreaming);
2892
- btn.disabled = !isConnected;
2893
- });
2894
-
2895
- if (this.ui.sendButton) this.ui.sendButton.style.display = isStreaming ? 'none' : '';
2896
- }
2897
-
2898
- removeScrollUpDetection() {
2899
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
2900
- if (scrollContainer && this._scrollUpHandler) {
2901
- scrollContainer.removeEventListener('scroll', this._scrollUpHandler);
2902
- this._scrollUpHandler = null;
2903
- }
2904
- }
2905
-
2906
- setupScrollUpDetection(conversationId) {
2907
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
2908
- if (!scrollContainer) return;
2909
-
2910
- if (!this._scrollDetectionState) this._scrollDetectionState = {};
2911
-
2912
- const detectionState = {
2913
- isLoading: false,
2914
- oldestTimestamp: Date.now(),
2915
- oldestMessageId: null,
2916
- conversation: conversationId
2917
- };
2918
-
2919
- const handleScroll = async () => {
2920
- const scrollTop = scrollContainer.scrollTop;
2921
- const scrollHeight = scrollContainer.scrollHeight;
2922
- const clientHeight = scrollContainer.clientHeight;
2923
- const THRESHOLD = 300;
2924
-
2925
- if (scrollTop < THRESHOLD && !detectionState.isLoading && scrollHeight > clientHeight) {
2926
- detectionState.isLoading = true;
2927
-
2928
- try {
2929
- const messagesEl = document.querySelector('.conversation-messages');
2930
- if (!messagesEl) {
2931
- detectionState.isLoading = false;
2932
- return;
2933
- }
2934
-
2935
- const firstMessageEl = messagesEl.querySelector('.message[data-msg-id]');
2936
- if (!firstMessageEl) {
2937
- const firstChunkEl = messagesEl.querySelector('[data-chunk-created]');
2938
- if (firstChunkEl) {
2939
- detectionState.oldestTimestamp = parseInt(firstChunkEl.getAttribute('data-chunk-created')) || 0;
2940
- }
2941
- } else {
2942
- detectionState.oldestMessageId = firstMessageEl.getAttribute('data-msg-id');
2943
- }
2944
-
2945
- let result;
2946
- if (detectionState.oldestMessageId) {
2947
- try {
2948
- result = await window.wsClient.rpc('msg.ls.earlier', {
2949
- id: conversationId,
2950
- before: detectionState.oldestMessageId,
2951
- limit: 50
2952
- });
2953
- } catch (e) {
2954
- const base = window.__BASE_URL || '';
2955
- const r = await fetch(`${base}/api/conversations/${conversationId}/messages?limit=50`);
2956
- const d = await r.json();
2957
- result = { messages: d.messages || [] };
2958
- }
2959
- } else if (detectionState.oldestTimestamp > 0) {
2960
- try {
2961
- result = await window.wsClient.rpc('conv.chunks.earlier', {
2962
- id: conversationId,
2963
- before: detectionState.oldestTimestamp,
2964
- limit: 500
2965
- });
2966
- } catch (e) {
2967
- const base = window.__BASE_URL || '';
2968
- const r = await fetch(`${base}/api/conversations/${conversationId}/chunks`);
2969
- const d = await r.json();
2970
- result = { chunks: d.chunks || [] };
2971
- }
2972
- }
2973
-
2974
- if (result && ((result.messages && result.messages.length > 0) || (result.chunks && result.chunks.length > 0))) {
2975
- const scrollHeightBefore = scrollContainer.scrollHeight;
2976
- const scrollTopBefore = scrollContainer.scrollTop;
2977
- const newContent = document.createDocumentFragment();
2978
-
2979
- if (result.messages && result.messages.length > 0) {
2980
- result.messages.forEach(msg => {
2981
- const div = document.createElement('div');
2982
- div.className = `message message-${msg.role}`;
2983
- div.setAttribute('data-msg-id', msg.id);
2984
- div.innerHTML = `<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>`;
2985
- newContent.appendChild(div);
2986
- });
2987
- }
2988
-
2989
- if (result.chunks && result.chunks.length > 0) {
2990
- result.chunks.forEach(chunk => {
2991
- const blockEl = this.renderer.renderBlock(chunk.data, {}, false);
2992
- if (blockEl) {
2993
- const wrapper = document.createElement('div');
2994
- wrapper.setAttribute('data-chunk-created', chunk.created_at);
2995
- wrapper.appendChild(blockEl);
2996
- newContent.appendChild(wrapper);
2997
- }
2998
- });
2999
- }
3000
-
3001
- if (messagesEl.firstChild) {
3002
- messagesEl.insertBefore(newContent, messagesEl.firstChild);
3003
- } else {
3004
- messagesEl.appendChild(newContent);
3005
- }
3006
-
3007
- const scrollHeightAfter = scrollContainer.scrollHeight;
3008
- scrollContainer.scrollTop = scrollTopBefore + (scrollHeightAfter - scrollHeightBefore);
3009
- }
3010
- } catch (error) {
3011
- console.error('Failed to load earlier messages:', error);
3012
- } finally {
3013
- detectionState.isLoading = false;
3014
- }
3015
- }
3016
- };
3017
-
3018
- scrollContainer.removeEventListener('scroll', this._scrollUpHandler);
3019
- this._scrollUpHandler = handleScroll;
3020
- scrollContainer.addEventListener('scroll', this._scrollUpHandler, { passive: true });
3021
- }
3022
-
3023
- renderMessagesFragment(messages) {
3024
- const frag = document.createDocumentFragment();
3025
- if (messages.length === 0) {
3026
- const p = document.createElement('p');
3027
- p.className = 'text-secondary';
3028
- p.textContent = 'No messages in this conversation yet';
3029
- frag.appendChild(p);
3030
- return frag;
3031
- }
3032
- for (const msg of messages) {
3033
- const div = document.createElement('div');
3034
- div.className = `message message-${msg.role}`;
3035
- div.innerHTML = `<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>`;
3036
- frag.appendChild(div);
3037
- }
3038
- return frag;
3039
- }
3040
-
3041
- renderMessages(messages) {
3042
- if (messages.length === 0) {
3043
- return '<p class="text-secondary">No messages in this conversation yet</p>';
3044
- }
3045
- return messages.map(msg => `<div class="message message-${msg.role}"><div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div></div>`).join('');
3046
- }
3047
-
3048
- escapeHtml(text) {
3049
- return window._escHtml(text);
3050
- }
3051
-
3052
- showError(message) {
3053
- console.error(message);
3054
- if (window.UIDialog) {
3055
- window.UIDialog.alert(message, 'Error');
3056
- }
3057
- }
3058
-
3059
- on(event, callback) {
3060
- if (!this.eventHandlers[event]) {
3061
- this.eventHandlers[event] = [];
3062
- }
3063
- this.eventHandlers[event].push(callback);
3064
- }
3065
-
3066
- emit(event, data) {
3067
- if (this.eventHandlers[event]) {
3068
- this.eventHandlers[event].forEach(callback => {
3069
- try {
3070
- callback(data);
3071
- } catch (error) {
3072
- console.error(`Event handler error for ${event}:`, error);
3073
- }
3074
- });
3075
- }
3076
- }
3077
-
3078
- getEffectiveAgentId() {
3079
- return this.ui.cliSelector?.value || null;
3080
- }
3081
-
3082
- getEffectiveSubAgent() {
3083
- if (this.ui.agentSelector?.value && this.ui.agentSelector.style.display !== 'none') {
3084
- return this.ui.agentSelector.value;
3085
- }
3086
- return null;
3087
- }
3088
-
3089
- getCurrentAgent() {
3090
- return this.getEffectiveAgentId();
3091
- }
3092
-
3093
- saveAgentAndModelToConversation() {
3094
- const convId = this.state.currentConversation?.id;
3095
- if (!convId || this._agentLocked) return;
3096
- const agentId = this.getEffectiveAgentId();
3097
- const subAgent = this.getEffectiveSubAgent();
3098
- const model = this.getCurrentModel();
3099
- window.wsClient.rpc('conv.upd', { id: convId, agentType: agentId, subAgent: subAgent || undefined, model: model || undefined }).catch(() => {});
3100
- }
3101
-
3102
- getCurrentModel() {
3103
- return this.ui.modelSelector?.value || null;
3104
- }
3105
-
3106
- getMetrics() {
3107
- return {
3108
- renderer: this.renderer.getMetrics(),
3109
- websocket: this.wsManager.getStatus(),
3110
- eventProcessor: this.eventProcessor.getStats(),
3111
- state: this.state
3112
- };
3113
- }
3114
-
3115
- saveDraftPrompt() {
3116
- const convId = this.state.currentConversation?.id;
3117
- if (convId && this.ui.messageInput) {
3118
- const draft = this.ui.messageInput.value;
3119
- this.draftPrompts.set(convId, draft);
3120
- if (draft) {
3121
- localStorage.setItem(`draft-${convId}`, draft);
3122
- }
3123
- }
3124
- }
3125
-
3126
- restoreDraftPrompt(conversationId) {
3127
- if (!this.ui.messageInput) return;
3128
-
3129
- let draft = this.draftPrompts.get(conversationId) || '';
3130
- if (!draft) {
3131
- draft = localStorage.getItem(`draft-${conversationId}`) || '';
3132
- if (draft) this.draftPrompts.set(conversationId, draft);
3133
- }
3134
-
3135
- this.ui.messageInput.value = draft;
3136
- }
3137
-
3138
- clearDraft(conversationId) {
3139
- this.draftPrompts.delete(conversationId);
3140
- localStorage.removeItem(`draft-${conversationId}`);
3141
- }
3142
-
3143
- updateSendButtonState() {
3144
- if (this.ui.sendButton) {
3145
- this.ui.sendButton.disabled = !this.wsManager.isConnected;
3146
- }
3147
- if (this.ui.injectButton && this.ui.injectButton.classList.contains('visible')) {
3148
- this.ui.injectButton.disabled = !this.wsManager.isConnected;
3149
- }
3150
- if (this.ui.queueButton && this.ui.queueButton.classList.contains('visible')) {
3151
- this.ui.queueButton.disabled = !this.wsManager.isConnected;
3152
- }
3153
- }
3154
-
3155
- disablePromptArea() {
3156
- }
3157
-
3158
- enablePromptArea() {
3159
- if (this.ui.messageInput) {
3160
- this.ui.messageInput.disabled = false;
3161
- }
3162
- const injectBtn = document.getElementById('injectBtn');
3163
- if (injectBtn) injectBtn.disabled = false;
3164
- }
3165
-
3166
- showStreamingPromptButtons() {
3167
- if (this.ui.injectButton) {
3168
- this.ui.injectButton.classList.add('visible');
3169
- this.ui.injectButton.disabled = !this.wsManager.isConnected;
3170
- }
3171
- if (this.ui.queueButton) {
3172
- this.ui.queueButton.classList.add('visible');
3173
- this.ui.queueButton.disabled = !this.wsManager.isConnected;
3174
- }
3175
- }
3176
-
3177
- ensurePromptAreaAlwaysEnabled() {
3178
- if (this.ui.messageInput) {
3179
- this.ui.messageInput.disabled = false;
3180
- }
3181
- }
3182
-
3183
- destroy() {
3184
-
3185
- this.renderer.destroy();
3186
- this.wsManager.destroy();
3187
- this.eventHandlers = {};
3188
- }
3189
- }
3190
-
3191
- window.__convPerfMetrics = () => {
3192
- const entries = performance.getEntriesByType('measure').filter(e => e.name.startsWith('conv-'));
3193
- return entries.map(e => ({ name: e.name, ms: Math.round(e.duration) }));
3194
- };
3195
-
3196
- let agentGUIClient = null;
3197
-
3198
- document.addEventListener('DOMContentLoaded', async () => {
3199
- try {
3200
- agentGUIClient = new AgentGUIClient();
3201
- window.agentGuiClient = agentGUIClient;
3202
- await agentGUIClient.init();
3203
- agentGUIClient._dbg('AgentGUI ready');
3204
- } catch (error) {
3205
- console.error('Failed to initialize AgentGUI:', error);
3206
- }
3207
- });
3208
-
3209
- if (typeof module !== 'undefined' && module.exports) {
3210
- module.exports = AgentGUIClient;
3211
- }
1
+
2
+ class AgentGUIClient {
3
+ constructor(config = {}) {
4
+ this.config = {
5
+ containerId: config.containerId || 'app',
6
+ outputContainerId: config.outputContainerId || 'output',
7
+ scrollContainerId: config.scrollContainerId || 'output-scroll',
8
+ autoConnect: config.autoConnect !== false,
9
+ ...config
10
+ };
11
+
12
+ this.renderer = new StreamingRenderer(config.renderer || {});
13
+ this.wsManager = window.wsManager || new WebSocketManager(config.websocket || {});
14
+ if (!window.wsManager) window.wsManager = this.wsManager;
15
+ this.eventProcessor = new EventProcessor(config.eventProcessor || {});
16
+
17
+ this.state = {
18
+ isInitialized: false,
19
+ currentSession: null,
20
+ currentConversation: null,
21
+ streamingConversations: new Map(),
22
+ sessionEvents: [],
23
+ conversations: [],
24
+ agents: []
25
+ };
26
+
27
+ this.conversationCache = new Map();
28
+ this.MAX_CACHE_SIZE = 10;
29
+
30
+ this.conversationListCache = {
31
+ data: [],
32
+ timestamp: 0,
33
+ ttl: 30000
34
+ };
35
+
36
+ this.draftPrompts = new Map();
37
+
38
+ this.eventHandlers = {};
39
+
40
+ this.ui = {
41
+ statusIndicator: null,
42
+ messageInput: null,
43
+ sendButton: null,
44
+ cliSelector: null,
45
+ agentSelector: null,
46
+ modelSelector: null
47
+ };
48
+
49
+ this._agentLocked = false;
50
+ this._isLoadingConversation = false;
51
+ this._modelCache = new Map();
52
+
53
+ this._renderedSeqs = {};
54
+ this._inflightRequests = new Map();
55
+ this._previousConvAbort = null;
56
+
57
+ this._bgCache = new Map();
58
+ this.BG_CACHE_MAX = 50;
59
+
60
+ this._loadInProgress = {};
61
+ this._currentRequestId = 0;
62
+
63
+
64
+ this._scrollTarget = 0;
65
+ this._scrollAnimating = false;
66
+ this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
67
+
68
+
69
+ this._serverProcessingEstimate = 2000;
70
+ this._lastSendTime = 0;
71
+ this._countdownTimer = null;
72
+
73
+ this.routerState = {
74
+ currentConversationId: null,
75
+ currentSessionId: null
76
+ };
77
+
78
+ this._debug = typeof localStorage !== 'undefined' && localStorage.getItem('debug') === '1';
79
+ }
80
+
81
+ _dbg(...args) { if (this._debug) console.log('[AgentGUI]', ...args); }
82
+
83
+ async init() {
84
+ try {
85
+ this._dbg('Initializing AgentGUI client');
86
+
87
+ const wsReady = this.config.autoConnect ? this.connectWebSocket() : Promise.resolve();
88
+
89
+ this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
90
+
91
+ if (typeof ImageLoader !== 'undefined') {
92
+ window.imageLoader = new ImageLoader();
93
+ }
94
+
95
+ this.setupWebSocketListeners();
96
+ this.setupRendererListeners();
97
+ this.setupUI();
98
+
99
+ await wsReady;
100
+ await Promise.all([
101
+ this.loadAgents(),
102
+ this.loadConversations(),
103
+ this.checkSpeechStatus()
104
+ ]);
105
+
106
+ this.enableControls();
107
+
108
+ this.restoreStateFromUrl();
109
+
110
+ this.state.isInitialized = true;
111
+ this.emit('initialized');
112
+ this._setupDebugHooks();
113
+
114
+ this._dbg('AgentGUI client initialized');
115
+ return this;
116
+ } catch (error) {
117
+ console.error('Client initialization error:', error);
118
+ this.showError('Failed to initialize client: ' + error.message);
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ }
124
+
125
+ window.__convPerfMetrics = () => {
126
+ const entries = performance.getEntriesByType('measure').filter(e => e.name.startsWith('conv-'));
127
+ return entries.map(e => ({ name: e.name, ms: Math.round(e.duration) }));
128
+ };
129
+
130
+ let agentGUIClient = null;
131
+
132
+ document.addEventListener('DOMContentLoaded', async () => {
133
+ try {
134
+ agentGUIClient = new AgentGUIClient();
135
+ window.agentGuiClient = agentGUIClient;
136
+ await agentGUIClient.init();
137
+ agentGUIClient._dbg('AgentGUI ready');
138
+ } catch (error) {
139
+ console.error('Failed to initialize AgentGUI:', error);
140
+ }
141
+ });
142
+
143
+ if (typeof module !== 'undefined' && module.exports) {
144
+ module.exports = AgentGUIClient;
145
+ }