agentgui 1.0.814 → 1.0.816
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/CLAUDE.md +2 -1
- package/package.json +1 -1
- package/static/js/client.js +0 -272
- package/static/js/conv-machine.js +0 -3
- package/static/js/conversations.js +0 -5
- package/static/js/event-filter.js +0 -59
- package/static/js/image-loader.js +0 -35
- package/static/js/script-runner.js +0 -3
- package/static/js/state-barrier.js +0 -4
- package/static/js/streaming-renderer.js +0 -273
- package/static/js/syntax-highlighter.js +0 -55
- package/static/js/ui-components.js +0 -51
- package/static/js/websocket-manager.js +0 -7
- package/static/js/ws-client.js +0 -4
- package/static/js/ws-machine.js +0 -2
- package/static/theme.js +0 -8
package/static/js/client.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentGUI Client
|
|
3
|
-
* Main application orchestrator that integrates WebSocket, event processing,
|
|
4
|
-
* and streaming renderer for real-time Claude Code execution visualization
|
|
5
|
-
*/
|
|
6
1
|
|
|
7
2
|
class AgentGUIClient {
|
|
8
3
|
constructor(config = {}) {
|
|
@@ -14,13 +9,11 @@ class AgentGUIClient {
|
|
|
14
9
|
...config
|
|
15
10
|
};
|
|
16
11
|
|
|
17
|
-
// Initialize components - reuse global wsManager/wsClient if available
|
|
18
12
|
this.renderer = new StreamingRenderer(config.renderer || {});
|
|
19
13
|
this.wsManager = window.wsManager || new WebSocketManager(config.websocket || {});
|
|
20
14
|
if (!window.wsManager) window.wsManager = this.wsManager;
|
|
21
15
|
this.eventProcessor = new EventProcessor(config.eventProcessor || {});
|
|
22
16
|
|
|
23
|
-
// Application state
|
|
24
17
|
this.state = {
|
|
25
18
|
isInitialized: false,
|
|
26
19
|
currentSession: null,
|
|
@@ -31,24 +24,19 @@ class AgentGUIClient {
|
|
|
31
24
|
agents: []
|
|
32
25
|
};
|
|
33
26
|
|
|
34
|
-
// Conversation DOM cache: store rendered DOM + scroll position per conversationId
|
|
35
27
|
this.conversationCache = new Map();
|
|
36
28
|
this.MAX_CACHE_SIZE = 10;
|
|
37
29
|
|
|
38
|
-
// Conversation list cache with TTL
|
|
39
30
|
this.conversationListCache = {
|
|
40
31
|
data: [],
|
|
41
32
|
timestamp: 0,
|
|
42
33
|
ttl: 30000 // 30 seconds
|
|
43
34
|
};
|
|
44
35
|
|
|
45
|
-
// Draft prompts per conversation
|
|
46
36
|
this.draftPrompts = new Map();
|
|
47
37
|
|
|
48
|
-
// Event handlers
|
|
49
38
|
this.eventHandlers = {};
|
|
50
39
|
|
|
51
|
-
// UI state
|
|
52
40
|
this.ui = {
|
|
53
41
|
statusIndicator: null,
|
|
54
42
|
messageInput: null,
|
|
@@ -66,12 +54,9 @@ class AgentGUIClient {
|
|
|
66
54
|
this._inflightRequests = new Map();
|
|
67
55
|
this._previousConvAbort = null;
|
|
68
56
|
|
|
69
|
-
// Background conversation cache: keeps last 50 conversations' streaming blocks in memory
|
|
70
|
-
// Map<conversationId, { items: {seq,packed}[], seqSet: Set<number>, sessionId: string }>
|
|
71
57
|
this._bgCache = new Map();
|
|
72
58
|
this.BG_CACHE_MAX = 50;
|
|
73
59
|
|
|
74
|
-
// PHASE 2: Request Lifetime Tracking
|
|
75
60
|
this._loadInProgress = {}; // { [conversationId]: { requestId, abortController, timestamp, prevConversationId } }
|
|
76
61
|
this._currentRequestId = 0; // Auto-incrementing request counter
|
|
77
62
|
|
|
@@ -85,7 +70,6 @@ class AgentGUIClient {
|
|
|
85
70
|
this._lastSendTime = 0;
|
|
86
71
|
this._countdownTimer = null;
|
|
87
72
|
|
|
88
|
-
// Router state
|
|
89
73
|
this.routerState = {
|
|
90
74
|
currentConversationId: null,
|
|
91
75
|
currentSessionId: null
|
|
@@ -96,17 +80,12 @@ class AgentGUIClient {
|
|
|
96
80
|
|
|
97
81
|
_dbg(...args) { if (this._debug) console.log('[AgentGUI]', ...args); }
|
|
98
82
|
|
|
99
|
-
/**
|
|
100
|
-
* Initialize the client
|
|
101
|
-
*/
|
|
102
83
|
async init() {
|
|
103
84
|
try {
|
|
104
85
|
this._dbg('Initializing AgentGUI client');
|
|
105
86
|
|
|
106
|
-
// Start WebSocket connection immediately (don't wait for UI setup)
|
|
107
87
|
const wsReady = this.config.autoConnect ? this.connectWebSocket() : Promise.resolve();
|
|
108
88
|
|
|
109
|
-
// Initialize renderer and UI in parallel with WS connection
|
|
110
89
|
this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
|
|
111
90
|
|
|
112
91
|
if (typeof ImageLoader !== 'undefined') {
|
|
@@ -117,7 +96,6 @@ class AgentGUIClient {
|
|
|
117
96
|
this.setupRendererListeners();
|
|
118
97
|
this.setupUI();
|
|
119
98
|
|
|
120
|
-
// Wait for WS, then load data in parallel
|
|
121
99
|
await wsReady;
|
|
122
100
|
await Promise.all([
|
|
123
101
|
this.loadAgents(),
|
|
@@ -125,10 +103,8 @@ class AgentGUIClient {
|
|
|
125
103
|
this.checkSpeechStatus()
|
|
126
104
|
]);
|
|
127
105
|
|
|
128
|
-
// Enable controls for initial interaction
|
|
129
106
|
this.enableControls();
|
|
130
107
|
|
|
131
|
-
// Restore state from URL on page load
|
|
132
108
|
this.restoreStateFromUrl();
|
|
133
109
|
|
|
134
110
|
this.state.isInitialized = true;
|
|
@@ -144,16 +120,11 @@ class AgentGUIClient {
|
|
|
144
120
|
}
|
|
145
121
|
}
|
|
146
122
|
|
|
147
|
-
/**
|
|
148
|
-
* Setup WebSocket event listeners
|
|
149
|
-
*/
|
|
150
123
|
setupWebSocketListeners() {
|
|
151
124
|
this.wsManager.on('connected', () => {
|
|
152
125
|
this._dbg('WebSocket connected');
|
|
153
126
|
this.updateConnectionStatus('connected');
|
|
154
127
|
this._subscribeToConversationUpdates();
|
|
155
|
-
// On reconnect (not initial connect), invalidate current conversation's DOM
|
|
156
|
-
// cache so we fetch fresh chunks rather than serving potentially stale DOM.
|
|
157
128
|
if (this.wsManager.stats.totalReconnects > 0 && this.state.currentConversation?.id) {
|
|
158
129
|
this.invalidateCache(this.state.currentConversation.id);
|
|
159
130
|
}
|
|
@@ -164,7 +135,6 @@ class AgentGUIClient {
|
|
|
164
135
|
this.updateBusyPromptArea(this.state.currentConversation.id);
|
|
165
136
|
}
|
|
166
137
|
this.emit('ws:connected');
|
|
167
|
-
// Check if server was updated while client was loaded - reload if version changed
|
|
168
138
|
if (window.__SERVER_VERSION) {
|
|
169
139
|
fetch((window.__BASE_URL || '') + '/api/version').then(r => r.json()).then(d => {
|
|
170
140
|
if (d.version && d.version !== window.__SERVER_VERSION) {
|
|
@@ -210,10 +180,8 @@ class AgentGUIClient {
|
|
|
210
180
|
if (dot) dot.classList.remove('degrading');
|
|
211
181
|
});
|
|
212
182
|
|
|
213
|
-
// Switch to idle view when selecting non-streaming conversation
|
|
214
183
|
window.addEventListener('conversation-selected', (e) => {
|
|
215
184
|
const convId = e.detail.conversationId;
|
|
216
|
-
// Save draft from previous conversation before switching
|
|
217
185
|
this.saveDraftPrompt();
|
|
218
186
|
|
|
219
187
|
const isStreaming = this._convIsStreaming(convId);
|
|
@@ -221,11 +189,9 @@ class AgentGUIClient {
|
|
|
221
189
|
window.switchView('chat');
|
|
222
190
|
}
|
|
223
191
|
|
|
224
|
-
// Restore draft for new conversation synchronously (setTimeout caused races)
|
|
225
192
|
this.restoreDraftPrompt(convId);
|
|
226
193
|
});
|
|
227
194
|
|
|
228
|
-
// Preserve controls state across tab switches
|
|
229
195
|
window.addEventListener('view-switched', (e) => {
|
|
230
196
|
const view = e.detail.view;
|
|
231
197
|
if (view === 'chat') {
|
|
@@ -240,14 +206,12 @@ class AgentGUIClient {
|
|
|
240
206
|
});
|
|
241
207
|
}
|
|
242
208
|
|
|
243
|
-
// Authoritative streaming check: conv machine is source of truth, Map is fallback cache
|
|
244
209
|
_convIsStreaming(convId) {
|
|
245
210
|
if (!convId) return false;
|
|
246
211
|
if (typeof convMachineAPI !== 'undefined') return convMachineAPI.isStreaming(convId);
|
|
247
212
|
return this.state.streamingConversations.has(convId);
|
|
248
213
|
}
|
|
249
214
|
|
|
250
|
-
// Mark conversation as streaming in both machine and cache Map
|
|
251
215
|
_setConvStreaming(convId, streaming, sessionId, agentId) {
|
|
252
216
|
if (!convId) return;
|
|
253
217
|
if (streaming) {
|
|
@@ -259,9 +223,6 @@ class AgentGUIClient {
|
|
|
259
223
|
}
|
|
260
224
|
}
|
|
261
225
|
|
|
262
|
-
/**
|
|
263
|
-
* Setup renderer event listeners
|
|
264
|
-
*/
|
|
265
226
|
setupRendererListeners() {
|
|
266
227
|
this.renderer.on('batch:complete', (data) => {
|
|
267
228
|
this._dbg('Batch rendered:', data);
|
|
@@ -273,16 +234,10 @@ class AgentGUIClient {
|
|
|
273
234
|
});
|
|
274
235
|
}
|
|
275
236
|
|
|
276
|
-
/**
|
|
277
|
-
* Router state management: restore conversation from URL
|
|
278
|
-
* Format: /conversations/<conversationId>?session=<sessionId>
|
|
279
|
-
*/
|
|
280
237
|
restoreStateFromUrl() {
|
|
281
|
-
// Parse path-based URL: /conversations/<conversationId>
|
|
282
238
|
const pathMatch = window.location.pathname.match(/\/conversations\/([^\/]+)$/);
|
|
283
239
|
const conversationId = pathMatch ? pathMatch[1] : null;
|
|
284
240
|
|
|
285
|
-
// Session ID still in query params
|
|
286
241
|
const params = new URLSearchParams(window.location.search);
|
|
287
242
|
const sessionId = params.get('session');
|
|
288
243
|
|
|
@@ -298,13 +253,11 @@ class AgentGUIClient {
|
|
|
298
253
|
} else {
|
|
299
254
|
this.loadConversationMessages(conversationId).catch((err) => {
|
|
300
255
|
console.warn('Failed to restore conversation from URL, loading latest instead:', err);
|
|
301
|
-
// If the URL conversation doesn't exist, try loading the most recent conversation
|
|
302
256
|
if (this.state.conversations && this.state.conversations.length > 0) {
|
|
303
257
|
const latestConv = this.state.conversations[0];
|
|
304
258
|
this._dbg('Loading latest conversation instead:', latestConv.id);
|
|
305
259
|
return this.loadConversationMessages(latestConv.id);
|
|
306
260
|
} else {
|
|
307
|
-
// No conversations available - show welcome screen
|
|
308
261
|
this._showWelcomeScreen();
|
|
309
262
|
}
|
|
310
263
|
}).finally(() => {
|
|
@@ -312,25 +265,15 @@ class AgentGUIClient {
|
|
|
312
265
|
});
|
|
313
266
|
}
|
|
314
267
|
} else {
|
|
315
|
-
// No conversation in URL - show welcome screen
|
|
316
268
|
this._showWelcomeScreen();
|
|
317
269
|
}
|
|
318
270
|
}
|
|
319
271
|
|
|
320
|
-
/**
|
|
321
|
-
* Validate ID format to prevent XSS
|
|
322
|
-
* Alphanumeric, dash, underscore only
|
|
323
|
-
*/
|
|
324
272
|
isValidId(id) {
|
|
325
273
|
if (!id || typeof id !== 'string') return false;
|
|
326
274
|
return /^[a-zA-Z0-9_-]+$/.test(id) && id.length < 256;
|
|
327
275
|
}
|
|
328
276
|
|
|
329
|
-
/**
|
|
330
|
-
* Update URL when conversation is selected
|
|
331
|
-
* Uses History API (pushState) for clean URLs
|
|
332
|
-
* Format: /conversations/<conversationId>?session=<sessionId>
|
|
333
|
-
*/
|
|
334
277
|
updateUrlForConversation(conversationId, sessionId) {
|
|
335
278
|
if (!this.isValidId(conversationId)) return;
|
|
336
279
|
if (!this.routerState) return;
|
|
@@ -340,11 +283,9 @@ class AgentGUIClient {
|
|
|
340
283
|
this.routerState.currentSessionId = sessionId;
|
|
341
284
|
}
|
|
342
285
|
|
|
343
|
-
// Use path-based URL for conversation
|
|
344
286
|
const basePath = window.location.pathname.replace(/\/conversations\/[^\/]+$/, '').replace(/\/$/, '');
|
|
345
287
|
let url = `${basePath}/conversations/${conversationId}`;
|
|
346
288
|
|
|
347
|
-
// Session ID still in query params for optional state
|
|
348
289
|
if (sessionId && this.isValidId(sessionId)) {
|
|
349
290
|
url += `?session=${sessionId}`;
|
|
350
291
|
}
|
|
@@ -352,10 +293,6 @@ class AgentGUIClient {
|
|
|
352
293
|
window.history.pushState({ conversationId, sessionId }, '', url);
|
|
353
294
|
}
|
|
354
295
|
|
|
355
|
-
/**
|
|
356
|
-
* Save scroll position to localStorage
|
|
357
|
-
* Key format: scroll_<conversationId>
|
|
358
|
-
*/
|
|
359
296
|
saveScrollPosition(conversationId) {
|
|
360
297
|
if (!this.isValidId(conversationId)) return;
|
|
361
298
|
|
|
@@ -370,10 +307,6 @@ class AgentGUIClient {
|
|
|
370
307
|
}
|
|
371
308
|
}
|
|
372
309
|
|
|
373
|
-
/**
|
|
374
|
-
* Restore scroll position from localStorage
|
|
375
|
-
* Restores after conversation loads
|
|
376
|
-
*/
|
|
377
310
|
restoreScrollPosition(conversationId) {
|
|
378
311
|
if (!this.isValidId(conversationId)) return;
|
|
379
312
|
|
|
@@ -404,10 +337,6 @@ class AgentGUIClient {
|
|
|
404
337
|
}
|
|
405
338
|
}
|
|
406
339
|
|
|
407
|
-
/**
|
|
408
|
-
* Setup scroll position tracking
|
|
409
|
-
* Debounced to avoid excessive localStorage writes
|
|
410
|
-
*/
|
|
411
340
|
setupScrollTracking() {
|
|
412
341
|
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
413
342
|
if (!scrollContainer) return;
|
|
@@ -433,16 +362,12 @@ class AgentGUIClient {
|
|
|
433
362
|
});
|
|
434
363
|
}
|
|
435
364
|
|
|
436
|
-
/**
|
|
437
|
-
* Setup UI elements
|
|
438
|
-
*/
|
|
439
365
|
setupUI() {
|
|
440
366
|
const container = document.getElementById(this.config.containerId);
|
|
441
367
|
if (!container) {
|
|
442
368
|
throw new Error(`Container not found: ${this.config.containerId}`);
|
|
443
369
|
}
|
|
444
370
|
|
|
445
|
-
// Get references to key UI elements
|
|
446
371
|
this.ui.statusIndicator = document.querySelector('[data-status-indicator]');
|
|
447
372
|
this.ui.messageInput = document.querySelector('[data-message-input]');
|
|
448
373
|
this.ui.sendButton = document.querySelector('[data-send-button]');
|
|
@@ -450,13 +375,11 @@ class AgentGUIClient {
|
|
|
450
375
|
this.ui.agentSelector = document.querySelector('[data-agent-selector]');
|
|
451
376
|
this.ui.modelSelector = document.querySelector('[data-model-selector]');
|
|
452
377
|
|
|
453
|
-
// Auto-save drafts on input
|
|
454
378
|
if (this.ui.messageInput) {
|
|
455
379
|
this.ui.messageInput.addEventListener('input', () => {
|
|
456
380
|
this.saveDraftPrompt();
|
|
457
381
|
});
|
|
458
382
|
|
|
459
|
-
// Restore draft when conversation loads
|
|
460
383
|
const currentConvId = this.state.currentConversation?.id;
|
|
461
384
|
if (currentConvId) {
|
|
462
385
|
this.restoreDraftPrompt(currentConvId);
|
|
@@ -475,7 +398,6 @@ class AgentGUIClient {
|
|
|
475
398
|
|
|
476
399
|
if (this.ui.agentSelector) {
|
|
477
400
|
this.ui.agentSelector.addEventListener('change', () => {
|
|
478
|
-
// Load models for parent CLI agent when sub-agent changes
|
|
479
401
|
const parentAgentId = this.ui.cliSelector?.value;
|
|
480
402
|
if (parentAgentId) {
|
|
481
403
|
this.loadModelsForAgent(parentAgentId);
|
|
@@ -492,7 +414,6 @@ class AgentGUIClient {
|
|
|
492
414
|
});
|
|
493
415
|
}
|
|
494
416
|
|
|
495
|
-
// Setup event listeners
|
|
496
417
|
if (this.ui.sendButton) {
|
|
497
418
|
this.ui.sendButton.addEventListener('click', () => this.startExecution());
|
|
498
419
|
}
|
|
@@ -533,7 +454,6 @@ class AgentGUIClient {
|
|
|
533
454
|
this.ui.messageInput.style.height = 'auto';
|
|
534
455
|
}
|
|
535
456
|
|
|
536
|
-
// Stop agent and resume with new message
|
|
537
457
|
window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: steerMsg })
|
|
538
458
|
.catch(err => {
|
|
539
459
|
console.error('Failed to steer:', err);
|
|
@@ -557,7 +477,6 @@ class AgentGUIClient {
|
|
|
557
477
|
return;
|
|
558
478
|
}
|
|
559
479
|
try {
|
|
560
|
-
// Queue uses msg.send which will enqueue if streaming is active
|
|
561
480
|
const data = await window.wsClient.rpc('msg.send', { id: this.state.currentConversation.id, content: message });
|
|
562
481
|
this._dbg('Queue response:', data);
|
|
563
482
|
if (this.ui.messageInput) {
|
|
@@ -606,7 +525,6 @@ class AgentGUIClient {
|
|
|
606
525
|
}
|
|
607
526
|
});
|
|
608
527
|
|
|
609
|
-
// Setup theme toggle
|
|
610
528
|
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
611
529
|
if (themeToggle) {
|
|
612
530
|
themeToggle.addEventListener('click', () => this.toggleTheme());
|
|
@@ -639,7 +557,6 @@ class AgentGUIClient {
|
|
|
639
557
|
this.unlockAgentAndModel();
|
|
640
558
|
});
|
|
641
559
|
|
|
642
|
-
// Listen for conversation selection (deduplicate rapid clicks)
|
|
643
560
|
window.addEventListener('conversation-selected', async (event) => {
|
|
644
561
|
const conversationId = event.detail.conversationId;
|
|
645
562
|
if (this._isLoadingConversation && this._loadingConversationId === conversationId) return;
|
|
@@ -654,7 +571,6 @@ class AgentGUIClient {
|
|
|
654
571
|
}
|
|
655
572
|
});
|
|
656
573
|
|
|
657
|
-
// Listen for active conversation deletion
|
|
658
574
|
window.addEventListener('conversation-deselected', () => {
|
|
659
575
|
window.ConversationState?.clear('deselected');
|
|
660
576
|
this.state.currentConversation = null;
|
|
@@ -735,9 +651,6 @@ class AgentGUIClient {
|
|
|
735
651
|
});
|
|
736
652
|
}
|
|
737
653
|
|
|
738
|
-
/**
|
|
739
|
-
* Connect to WebSocket
|
|
740
|
-
*/
|
|
741
654
|
async connectWebSocket() {
|
|
742
655
|
try {
|
|
743
656
|
await this.wsManager.connect();
|
|
@@ -751,7 +664,6 @@ class AgentGUIClient {
|
|
|
751
664
|
|
|
752
665
|
handleWebSocketMessage(data) {
|
|
753
666
|
try {
|
|
754
|
-
// Dispatch to window so other modules (conversations.js) can listen
|
|
755
667
|
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }));
|
|
756
668
|
|
|
757
669
|
switch (data.type) {
|
|
@@ -834,10 +746,6 @@ class AgentGUIClient {
|
|
|
834
746
|
this._serverProcessingEstimate = 0.7 * this._serverProcessingEstimate + 0.3 * serverTime;
|
|
835
747
|
}
|
|
836
748
|
|
|
837
|
-
// Subscribe to the session so blocks are not lost, but skip conversation
|
|
838
|
-
// re-subscribe when resumed=true to prevent infinite loop (server sends
|
|
839
|
-
// streaming_start on subscribe when activeExecutions exists, which would
|
|
840
|
-
// trigger another subscribe here, looping forever)
|
|
841
749
|
if (this.wsManager.isConnected) {
|
|
842
750
|
this.wsManager.subscribeToSession(data.sessionId);
|
|
843
751
|
if (!data.resumed) {
|
|
@@ -845,8 +753,6 @@ class AgentGUIClient {
|
|
|
845
753
|
}
|
|
846
754
|
}
|
|
847
755
|
|
|
848
|
-
// If this streaming event is for a different conversation than what we are viewing,
|
|
849
|
-
// just track the state but do not modify the DOM or start polling
|
|
850
756
|
if (this.state.currentConversation?.id !== data.conversationId) {
|
|
851
757
|
this._dbg('Streaming started for non-active conversation:', data.conversationId);
|
|
852
758
|
this._setConvStreaming(data.conversationId, true, data.sessionId, data.agentId);
|
|
@@ -854,7 +760,6 @@ class AgentGUIClient {
|
|
|
854
760
|
this.updateBusyPromptArea(data.conversationId);
|
|
855
761
|
this.emit('streaming:start', data);
|
|
856
762
|
|
|
857
|
-
// Auto-load if no conversation is currently selected (e.g. server resumed on startup)
|
|
858
763
|
if (!this.state.currentConversation && !this._isLoadingConversation) {
|
|
859
764
|
this._isLoadingConversation = true;
|
|
860
765
|
this.loadConversationMessages(data.conversationId).finally(() => {
|
|
@@ -874,7 +779,6 @@ class AgentGUIClient {
|
|
|
874
779
|
};
|
|
875
780
|
this.state.sessionEvents = [];
|
|
876
781
|
|
|
877
|
-
// Update URL with session ID during streaming
|
|
878
782
|
this.updateUrlForConversation(data.conversationId, data.sessionId);
|
|
879
783
|
|
|
880
784
|
if (this.wsManager.isConnected) {
|
|
@@ -939,13 +843,10 @@ class AgentGUIClient {
|
|
|
939
843
|
}
|
|
940
844
|
}, 1000);
|
|
941
845
|
|
|
942
|
-
// Reset rendered block seq tracker for this session
|
|
943
846
|
this._renderedSeqs[data.sessionId] = new Set();
|
|
944
847
|
|
|
945
|
-
// Show queue/steer UI when streaming starts (for busy prompt)
|
|
946
848
|
this.showStreamingPromptButtons();
|
|
947
849
|
|
|
948
|
-
// IMMUTABLE: Prompt area remains enabled - user can queue/steer messages
|
|
949
850
|
this.emit('streaming:start', data);
|
|
950
851
|
}
|
|
951
852
|
|
|
@@ -968,7 +869,6 @@ class AgentGUIClient {
|
|
|
968
869
|
_handleStreamingProgressInner(data) {
|
|
969
870
|
if (!data.block || !data.sessionId) return;
|
|
970
871
|
|
|
971
|
-
// Deduplicate by seq number to guarantee exactly-once rendering
|
|
972
872
|
const seen = this._renderedSeqs[data.sessionId] || (this._renderedSeqs[data.sessionId] = new Set());
|
|
973
873
|
if (data.seq !== undefined) {
|
|
974
874
|
if (seen.has(data.seq)) return;
|
|
@@ -977,12 +877,10 @@ class AgentGUIClient {
|
|
|
977
877
|
|
|
978
878
|
const block = data.block;
|
|
979
879
|
|
|
980
|
-
// Cache block for background conversations (all 50 cached convs, not just active)
|
|
981
880
|
const convId = data.conversationId;
|
|
982
881
|
if (convId) {
|
|
983
882
|
let entry = this._bgCache.get(convId);
|
|
984
883
|
if (!entry) {
|
|
985
|
-
// Evict oldest if at capacity
|
|
986
884
|
if (this._bgCache.size >= this.BG_CACHE_MAX) {
|
|
987
885
|
const oldestKey = this._bgCache.keys().next().value;
|
|
988
886
|
this._bgCache.delete(oldestKey);
|
|
@@ -993,7 +891,6 @@ class AgentGUIClient {
|
|
|
993
891
|
if (data.seq === undefined || !entry.seqSet.has(data.seq)) {
|
|
994
892
|
if (data.seq !== undefined) entry.seqSet.add(data.seq);
|
|
995
893
|
entry.sessionId = data.sessionId;
|
|
996
|
-
// Store seq alongside packed data so _flushBgCache can dedup against _renderedSeqs
|
|
997
894
|
try {
|
|
998
895
|
const packed = typeof msgpackr !== 'undefined' ? msgpackr.pack(block) : block;
|
|
999
896
|
entry.items.push({ seq: data.seq, packed });
|
|
@@ -1001,7 +898,6 @@ class AgentGUIClient {
|
|
|
1001
898
|
}
|
|
1002
899
|
}
|
|
1003
900
|
|
|
1004
|
-
// Only render for the currently-visible session
|
|
1005
901
|
if (this.state.currentSession?.id !== data.sessionId) return;
|
|
1006
902
|
|
|
1007
903
|
const streamingEl = document.getElementById(`streaming-${data.sessionId}`);
|
|
@@ -1085,7 +981,6 @@ class AgentGUIClient {
|
|
|
1085
981
|
return this.escapeHtml(part.content);
|
|
1086
982
|
}).join('');
|
|
1087
983
|
}
|
|
1088
|
-
// Fallback for unknown block types: show formatted key-value pairs
|
|
1089
984
|
const fieldsHtml = Object.entries(block)
|
|
1090
985
|
.filter(([key]) => key !== 'type')
|
|
1091
986
|
.map(([key, value]) => {
|
|
@@ -1147,14 +1042,12 @@ class AgentGUIClient {
|
|
|
1147
1042
|
this._clearThinkingCountdown();
|
|
1148
1043
|
if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
|
|
1149
1044
|
|
|
1150
|
-
// Hide stop and inject buttons on error
|
|
1151
1045
|
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
1152
1046
|
if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
|
|
1153
1047
|
if (this.ui.sendButton) this.ui.sendButton.style.display = '';
|
|
1154
1048
|
|
|
1155
1049
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
1156
1050
|
|
|
1157
|
-
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
1158
1051
|
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
1159
1052
|
this._dbg('Streaming error for non-active conversation:', conversationId);
|
|
1160
1053
|
this._setConvStreaming(conversationId, false);
|
|
@@ -1166,11 +1059,9 @@ class AgentGUIClient {
|
|
|
1166
1059
|
this._setConvStreaming(conversationId, false);
|
|
1167
1060
|
this.updateBusyPromptArea(conversationId);
|
|
1168
1061
|
|
|
1169
|
-
// Clear queue indicator on error
|
|
1170
1062
|
const queueEl = document.querySelector('.queue-indicator');
|
|
1171
1063
|
if (queueEl) queueEl.remove();
|
|
1172
1064
|
|
|
1173
|
-
// If this is a premature ACP end, render distinct warning block
|
|
1174
1065
|
if (data.isPrematureEnd) {
|
|
1175
1066
|
this.renderer.queueEvent({
|
|
1176
1067
|
type: 'streaming_error',
|
|
@@ -1186,7 +1077,6 @@ class AgentGUIClient {
|
|
|
1186
1077
|
|
|
1187
1078
|
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
1188
1079
|
|
|
1189
|
-
// Remove all orphaned streaming indicators (handles case where session never started)
|
|
1190
1080
|
const outputEl2 = document.getElementById('output');
|
|
1191
1081
|
if (outputEl2) {
|
|
1192
1082
|
outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => {
|
|
@@ -1201,7 +1091,6 @@ class AgentGUIClient {
|
|
|
1201
1091
|
if (indicator) {
|
|
1202
1092
|
indicator.innerHTML = `<span style="color:var(--color-error);">Error: ${this.escapeHtml(data.error || 'Unknown error')}</span>`;
|
|
1203
1093
|
}
|
|
1204
|
-
// Remove all thinking blocks on error
|
|
1205
1094
|
streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
|
|
1206
1095
|
} else {
|
|
1207
1096
|
const outputEl3 = document.getElementById('output');
|
|
@@ -1244,24 +1133,19 @@ class AgentGUIClient {
|
|
|
1244
1133
|
|
|
1245
1134
|
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
1246
1135
|
|
|
1247
|
-
// Unsubscribe from session to prevent subscription leak
|
|
1248
1136
|
if (sessionId && this.wsManager) {
|
|
1249
1137
|
try {
|
|
1250
1138
|
this.wsManager.unsubscribeFromSession(sessionId);
|
|
1251
1139
|
} catch (e) {
|
|
1252
|
-
// Session may not exist, ignore
|
|
1253
1140
|
}
|
|
1254
1141
|
}
|
|
1255
1142
|
|
|
1256
|
-
// Clear queue indicator when streaming completes
|
|
1257
1143
|
const queueEl = document.querySelector('.queue-indicator');
|
|
1258
1144
|
if (queueEl) queueEl.remove();
|
|
1259
1145
|
|
|
1260
|
-
// Remove ALL streaming indicators from the entire messages container
|
|
1261
1146
|
const outputEl2 = document.getElementById('output');
|
|
1262
1147
|
if (outputEl2) {
|
|
1263
1148
|
outputEl2.querySelectorAll('.streaming-indicator').forEach(ind => ind.remove());
|
|
1264
|
-
// Remove session start/complete blocks that clutter the chat
|
|
1265
1149
|
outputEl2.querySelectorAll('.event-streaming-start, .event-streaming-complete').forEach(block => block.remove());
|
|
1266
1150
|
}
|
|
1267
1151
|
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
@@ -1270,7 +1154,6 @@ class AgentGUIClient {
|
|
|
1270
1154
|
const prevTextEl = streamingEl.querySelector('.streaming-text-current');
|
|
1271
1155
|
if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
|
|
1272
1156
|
|
|
1273
|
-
// Remove all thinking blocks (block-thinking elements)
|
|
1274
1157
|
streamingEl.querySelectorAll('.block-thinking').forEach(block => block.remove());
|
|
1275
1158
|
|
|
1276
1159
|
const ts = document.createElement('div');
|
|
@@ -1283,7 +1166,6 @@ class AgentGUIClient {
|
|
|
1283
1166
|
this.saveScrollPosition(conversationId);
|
|
1284
1167
|
}
|
|
1285
1168
|
|
|
1286
|
-
// Recover any blocks missed during streaming (e.g. WS reconnects)
|
|
1287
1169
|
this._recoverMissedChunks().catch(err => {
|
|
1288
1170
|
console.warn('Chunk recovery failed:', err.message);
|
|
1289
1171
|
});
|
|
@@ -1308,9 +1190,6 @@ class AgentGUIClient {
|
|
|
1308
1190
|
}
|
|
1309
1191
|
}
|
|
1310
1192
|
|
|
1311
|
-
/**
|
|
1312
|
-
* Handle conversation created
|
|
1313
|
-
*/
|
|
1314
1193
|
handleConversationCreated(data) {
|
|
1315
1194
|
if (data.conversation) {
|
|
1316
1195
|
if (this.state.conversations.some(c => c.id === data.conversation.id)) {
|
|
@@ -1329,7 +1208,6 @@ class AgentGUIClient {
|
|
|
1329
1208
|
|
|
1330
1209
|
this._dbg('[SYNC] message_created:', { msgId: data.message.id, role: data.message.role, convId: data.conversationId });
|
|
1331
1210
|
|
|
1332
|
-
// Update messageCount in current conversation state for user messages
|
|
1333
1211
|
if (data.message.role === 'user' && this.state.currentConversation) {
|
|
1334
1212
|
this.state.currentConversation.messageCount = (this.state.currentConversation.messageCount || 0) + 1;
|
|
1335
1213
|
}
|
|
@@ -1346,7 +1224,6 @@ class AgentGUIClient {
|
|
|
1346
1224
|
}
|
|
1347
1225
|
|
|
1348
1226
|
if (data.message.role === 'user') {
|
|
1349
|
-
// Find pending message by matching content to avoid duplicates
|
|
1350
1227
|
const pending = outputEl.querySelector('.message-sending');
|
|
1351
1228
|
if (pending) {
|
|
1352
1229
|
pending.id = '';
|
|
@@ -1360,7 +1237,6 @@ class AgentGUIClient {
|
|
|
1360
1237
|
this.emit('message:created', data);
|
|
1361
1238
|
return;
|
|
1362
1239
|
}
|
|
1363
|
-
// Also check for pending ID (in case message-sending was already removed by _confirmOptimisticMessage)
|
|
1364
1240
|
const pendingById = outputEl.querySelector('[id^="pending-"]');
|
|
1365
1241
|
if (pendingById) {
|
|
1366
1242
|
pendingById.id = '';
|
|
@@ -1373,7 +1249,6 @@ class AgentGUIClient {
|
|
|
1373
1249
|
this.emit('message:created', data);
|
|
1374
1250
|
return;
|
|
1375
1251
|
}
|
|
1376
|
-
// Check if a user message with this ID already exists (prevents duplicate on race condition)
|
|
1377
1252
|
const existingMsg = outputEl.querySelector(`[data-msg-id="${data.message.id}"]`);
|
|
1378
1253
|
if (existingMsg) {
|
|
1379
1254
|
this.emit('message:created', data);
|
|
@@ -1397,11 +1272,9 @@ class AgentGUIClient {
|
|
|
1397
1272
|
}
|
|
1398
1273
|
|
|
1399
1274
|
handleConversationUpdated(data) {
|
|
1400
|
-
// Update current conversation metadata if this is the active conversation
|
|
1401
1275
|
if (data.conversation && data.conversation.id === this.state.currentConversation?.id) {
|
|
1402
1276
|
this.state.currentConversation = data.conversation;
|
|
1403
1277
|
}
|
|
1404
|
-
// Emit event for sidebar/other listeners
|
|
1405
1278
|
this.emit('conversation:updated', data);
|
|
1406
1279
|
}
|
|
1407
1280
|
|
|
@@ -1418,8 +1291,6 @@ class AgentGUIClient {
|
|
|
1418
1291
|
|
|
1419
1292
|
handleQueueItemDequeued(data) {
|
|
1420
1293
|
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
1421
|
-
// Item was dequeued and execution started - remove from queue indicator
|
|
1422
|
-
// and update queue display
|
|
1423
1294
|
this.fetchAndRenderQueue(data.conversationId);
|
|
1424
1295
|
}
|
|
1425
1296
|
|
|
@@ -1579,9 +1450,6 @@ class AgentGUIClient {
|
|
|
1579
1450
|
return parts;
|
|
1580
1451
|
}
|
|
1581
1452
|
|
|
1582
|
-
/**
|
|
1583
|
-
* Render a markdown code block part
|
|
1584
|
-
*/
|
|
1585
1453
|
renderCodeBlock(language, code) {
|
|
1586
1454
|
if (language.toLowerCase() === 'html') {
|
|
1587
1455
|
return `
|
|
@@ -1600,9 +1468,6 @@ class AgentGUIClient {
|
|
|
1600
1468
|
}
|
|
1601
1469
|
}
|
|
1602
1470
|
|
|
1603
|
-
/**
|
|
1604
|
-
* Render message content based on type
|
|
1605
|
-
*/
|
|
1606
1471
|
renderMessageContent(content) {
|
|
1607
1472
|
if (typeof content === 'string') {
|
|
1608
1473
|
if (this.isHtmlContent(content)) {
|
|
@@ -1633,7 +1498,6 @@ class AgentGUIClient {
|
|
|
1633
1498
|
}
|
|
1634
1499
|
});
|
|
1635
1500
|
} else if (block.type === 'code_block') {
|
|
1636
|
-
// Render HTML code blocks as actual HTML elements
|
|
1637
1501
|
if (block.language === 'html') {
|
|
1638
1502
|
html += `
|
|
1639
1503
|
<div class="message-code">
|
|
@@ -1702,7 +1566,6 @@ class AgentGUIClient {
|
|
|
1702
1566
|
html += '</div>';
|
|
1703
1567
|
return html;
|
|
1704
1568
|
} else {
|
|
1705
|
-
// Fallback for non-array content: format as key-value pairs
|
|
1706
1569
|
if (typeof content === 'object' && content !== null) {
|
|
1707
1570
|
const fieldsHtml = Object.entries(content)
|
|
1708
1571
|
.map(([key, value]) => {
|
|
@@ -1734,7 +1597,6 @@ class AgentGUIClient {
|
|
|
1734
1597
|
|
|
1735
1598
|
const pendingId = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
|
|
1736
1599
|
|
|
1737
|
-
// Conv machine is authoritative: check machine state for optimistic message gating
|
|
1738
1600
|
const isStreaming = this._convIsStreaming(this.state.currentConversation?.id);
|
|
1739
1601
|
if (!isStreaming) {
|
|
1740
1602
|
this._showOptimisticMessage(pendingId, savedPrompt);
|
|
@@ -1764,7 +1626,6 @@ class AgentGUIClient {
|
|
|
1764
1626
|
this.lockAgentAndModel(agentId, model);
|
|
1765
1627
|
await this.streamToConversation(conv.id, savedPrompt, agentId, model, subAgent);
|
|
1766
1628
|
this.clearDraft(conv.id);
|
|
1767
|
-
// Only confirm optimistic message if it was shown (not queued)
|
|
1768
1629
|
if (!isStreaming) {
|
|
1769
1630
|
this._confirmOptimisticMessage(pendingId);
|
|
1770
1631
|
}
|
|
@@ -1792,7 +1653,6 @@ class AgentGUIClient {
|
|
|
1792
1653
|
}
|
|
1793
1654
|
} catch (error) {
|
|
1794
1655
|
console.error('Execution error:', error);
|
|
1795
|
-
// Only fail optimistic message if it was shown
|
|
1796
1656
|
if (!isStreaming) {
|
|
1797
1657
|
this._failOptimisticMessage(pendingId, savedPrompt, error.message);
|
|
1798
1658
|
}
|
|
@@ -1845,7 +1705,6 @@ class AgentGUIClient {
|
|
|
1845
1705
|
}
|
|
1846
1706
|
}
|
|
1847
1707
|
|
|
1848
|
-
// Flush background-cached blocks into the active streaming container
|
|
1849
1708
|
_flushBgCache(conversationId, sessionId) {
|
|
1850
1709
|
const entry = this._bgCache.get(conversationId);
|
|
1851
1710
|
if (!entry || entry.items.length === 0) return;
|
|
@@ -1858,7 +1717,6 @@ class AgentGUIClient {
|
|
|
1858
1717
|
|
|
1859
1718
|
const seenSeqs = this._renderedSeqs[sessionId] || (this._renderedSeqs[sessionId] = new Set());
|
|
1860
1719
|
for (const item of entry.items) {
|
|
1861
|
-
// Skip blocks already rendered (dedup by seq)
|
|
1862
1720
|
if (item.seq !== undefined && seenSeqs.has(item.seq)) continue;
|
|
1863
1721
|
try {
|
|
1864
1722
|
const block = (typeof msgpackr !== 'undefined' && item.packed instanceof Uint8Array)
|
|
@@ -1876,13 +1734,8 @@ class AgentGUIClient {
|
|
|
1876
1734
|
|
|
1877
1735
|
async _recoverMissedChunks() {
|
|
1878
1736
|
if (!this.state.currentSession?.id) return;
|
|
1879
|
-
// Note: do NOT gate on streamingConversations - this is called from handleStreamingComplete
|
|
1880
|
-
// where we've already removed the conversation from the set. Allow recovery always.
|
|
1881
1737
|
|
|
1882
1738
|
const sessionId = this.state.currentSession.id;
|
|
1883
|
-
// Use lastSeq=-1 when no WS messages received yet (fresh load/full disconnect).
|
|
1884
|
-
// Server query is `sequence > sinceSeq`, so -1 returns all chunks from seq 0.
|
|
1885
|
-
// _renderedSeqs dedup prevents double-rendering anything already shown.
|
|
1886
1739
|
const lastSeq = this.wsManager.getLastSeq(sessionId);
|
|
1887
1740
|
|
|
1888
1741
|
try {
|
|
@@ -1991,7 +1844,6 @@ class AgentGUIClient {
|
|
|
1991
1844
|
latencyTrend: self.wsManager?.latency?.trend || null
|
|
1992
1845
|
}),
|
|
1993
1846
|
|
|
1994
|
-
// Sync-to-display debugging
|
|
1995
1847
|
getSyncState: () => ({
|
|
1996
1848
|
currentConversation: self.state.currentConversation,
|
|
1997
1849
|
isStreaming: self._convIsStreaming(self.state.currentConversation?.id),
|
|
@@ -2006,7 +1858,6 @@ class AgentGUIClient {
|
|
|
2006
1858
|
rendererEventHistoryLength: self.renderer?.eventHistory?.length || 0,
|
|
2007
1859
|
}),
|
|
2008
1860
|
|
|
2009
|
-
// Message DOM state
|
|
2010
1861
|
getMessageState: () => {
|
|
2011
1862
|
const output = document.querySelector('.conversation-messages');
|
|
2012
1863
|
if (!output) return { error: 'No conversation output found' };
|
|
@@ -2018,21 +1869,14 @@ class AgentGUIClient {
|
|
|
2018
1869
|
};
|
|
2019
1870
|
}
|
|
2020
1871
|
|
|
2021
|
-
/**
|
|
2022
|
-
* Show native loading spinner on document element
|
|
2023
|
-
*/
|
|
2024
1872
|
showLoadingSpinner() {
|
|
2025
1873
|
document.documentElement.style.pointerEvents = 'auto';
|
|
2026
|
-
// Show native CSS loading indicator (not removing, just visual cue)
|
|
2027
1874
|
const indicator = document.querySelector('[data-model-dl-indicator]');
|
|
2028
1875
|
if (indicator && !indicator.classList.contains('visible')) {
|
|
2029
1876
|
indicator.classList.add('visible');
|
|
2030
1877
|
}
|
|
2031
1878
|
}
|
|
2032
1879
|
|
|
2033
|
-
/**
|
|
2034
|
-
* Hide native loading spinner
|
|
2035
|
-
*/
|
|
2036
1880
|
hideLoadingSpinner() {
|
|
2037
1881
|
const indicator = document.querySelector('[data-model-dl-indicator]');
|
|
2038
1882
|
if (indicator && indicator.classList.contains('visible')) {
|
|
@@ -2040,13 +1884,9 @@ class AgentGUIClient {
|
|
|
2040
1884
|
}
|
|
2041
1885
|
}
|
|
2042
1886
|
|
|
2043
|
-
/**
|
|
2044
|
-
* Show welcome screen when no conversation is selected
|
|
2045
|
-
*/
|
|
2046
1887
|
_showWelcomeScreen() {
|
|
2047
1888
|
const outputEl = document.getElementById('output');
|
|
2048
1889
|
if (!outputEl) return;
|
|
2049
|
-
// Build agent options from loaded agents list
|
|
2050
1890
|
const agents = this.state.agents || [];
|
|
2051
1891
|
const agentOptions = agents.map(a =>
|
|
2052
1892
|
`<option value="${this.escapeHtml(a.id)}">${this.escapeHtml(a.name.split(/[\s\-]+/)[0])}</option>`
|
|
@@ -2077,7 +1917,6 @@ class AgentGUIClient {
|
|
|
2077
1917
|
</div>
|
|
2078
1918
|
</div>
|
|
2079
1919
|
`;
|
|
2080
|
-
// Sync welcome agent select with the bottom bar cli selector
|
|
2081
1920
|
const welcomeSel = document.getElementById('welcomeAgentSelect');
|
|
2082
1921
|
if (welcomeSel) {
|
|
2083
1922
|
if (this.ui.cliSelector) welcomeSel.value = this.ui.cliSelector.value;
|
|
@@ -2180,7 +2019,6 @@ class AgentGUIClient {
|
|
|
2180
2019
|
blockFrag.appendChild(el);
|
|
2181
2020
|
}
|
|
2182
2021
|
blocksEl.appendChild(blockFrag);
|
|
2183
|
-
// Build tool-use element index once for O(1) lookups instead of O(n) querySelectorAll per chunk
|
|
2184
2022
|
const toolUseIndex = new Map();
|
|
2185
2023
|
blocksEl.querySelectorAll('.block-tool-use[data-tool-use-id]').forEach(el => toolUseIndex.set(el.dataset.toolUseId, el));
|
|
2186
2024
|
for (const chunk of deferred) {
|
|
@@ -2316,9 +2154,6 @@ class AgentGUIClient {
|
|
|
2316
2154
|
this.scrollToBottom();
|
|
2317
2155
|
}
|
|
2318
2156
|
|
|
2319
|
-
/**
|
|
2320
|
-
* Load agents
|
|
2321
|
-
*/
|
|
2322
2157
|
async loadAgents() {
|
|
2323
2158
|
return this._dedupedFetch('loadAgents', async () => {
|
|
2324
2159
|
try {
|
|
@@ -2367,13 +2202,11 @@ class AgentGUIClient {
|
|
|
2367
2202
|
.join('');
|
|
2368
2203
|
this.ui.agentSelector.style.display = 'inline-block';
|
|
2369
2204
|
this._dbg(`[Agent Selector] Loaded ${subAgents.length} sub-agents for ${cliAgentId}`);
|
|
2370
|
-
// Auto-select first sub-agent and load its models
|
|
2371
2205
|
const firstSubAgentId = subAgents[0].id;
|
|
2372
2206
|
this.ui.agentSelector.value = firstSubAgentId;
|
|
2373
2207
|
this.loadModelsForAgent(cliAgentId); // models keyed to parent agent
|
|
2374
2208
|
} else {
|
|
2375
2209
|
this._dbg(`[Agent Selector] No sub-agents found for ${cliAgentId}`);
|
|
2376
|
-
// Load models for the CLI agent itself (fallback for agents without sub-agents)
|
|
2377
2210
|
const cliToAcpMap = {
|
|
2378
2211
|
'cli-opencode': 'opencode',
|
|
2379
2212
|
'cli-gemini': 'gemini',
|
|
@@ -2384,9 +2217,7 @@ class AgentGUIClient {
|
|
|
2384
2217
|
this.loadModelsForAgent(acpAgentId);
|
|
2385
2218
|
}
|
|
2386
2219
|
} catch (err) {
|
|
2387
|
-
// No sub-agents available for this CLI tool — keep hidden
|
|
2388
2220
|
console.warn(`[Agent Selector] Failed to load sub-agents for ${cliAgentId}:`, err.message);
|
|
2389
|
-
// Fallback: load models for the corresponding ACP agent
|
|
2390
2221
|
const cliToAcpMap = {
|
|
2391
2222
|
'cli-opencode': 'opencode',
|
|
2392
2223
|
'cli-gemini': 'gemini',
|
|
@@ -2474,10 +2305,6 @@ class AgentGUIClient {
|
|
|
2474
2305
|
}
|
|
2475
2306
|
}
|
|
2476
2307
|
|
|
2477
|
-
/**
|
|
2478
|
-
* Apply agent and model selection based on conversation state
|
|
2479
|
-
* Consolidates duplicate logic for cached and fresh conversation loads
|
|
2480
|
-
*/
|
|
2481
2308
|
applyAgentAndModelSelection(conversation, hasActivity) {
|
|
2482
2309
|
const agentId = conversation.agentId || conversation.agentType || null;
|
|
2483
2310
|
const model = conversation.model || null;
|
|
@@ -2513,11 +2340,7 @@ class AgentGUIClient {
|
|
|
2513
2340
|
}
|
|
2514
2341
|
}
|
|
2515
2342
|
|
|
2516
|
-
/**
|
|
2517
|
-
* Load conversations
|
|
2518
|
-
*/
|
|
2519
2343
|
async loadConversations() {
|
|
2520
|
-
// Return cached conversations if still fresh
|
|
2521
2344
|
const now = Date.now();
|
|
2522
2345
|
if (this.conversationListCache.data.length > 0 &&
|
|
2523
2346
|
(now - this.conversationListCache.timestamp) < this.conversationListCache.ttl) {
|
|
@@ -2529,7 +2352,6 @@ class AgentGUIClient {
|
|
|
2529
2352
|
try {
|
|
2530
2353
|
const { conversations } = await window.wsClient.rpc('conv.ls');
|
|
2531
2354
|
this.state.conversations = conversations;
|
|
2532
|
-
// Update cache
|
|
2533
2355
|
this.conversationListCache.data = conversations;
|
|
2534
2356
|
this.conversationListCache.timestamp = Date.now();
|
|
2535
2357
|
return conversations;
|
|
@@ -2540,9 +2362,6 @@ class AgentGUIClient {
|
|
|
2540
2362
|
});
|
|
2541
2363
|
}
|
|
2542
2364
|
|
|
2543
|
-
/**
|
|
2544
|
-
* Update connection status UI
|
|
2545
|
-
*/
|
|
2546
2365
|
updateConnectionStatus(status) {
|
|
2547
2366
|
if (this.ui.statusIndicator) {
|
|
2548
2367
|
this.ui.statusIndicator.dataset.status = status;
|
|
@@ -2578,7 +2397,6 @@ class AgentGUIClient {
|
|
|
2578
2397
|
const label = indicator.querySelector('.connection-label');
|
|
2579
2398
|
if (!dot || !label) return;
|
|
2580
2399
|
|
|
2581
|
-
// Check if model download is in progress
|
|
2582
2400
|
if (this._modelDownloadInProgress) {
|
|
2583
2401
|
dot.className = 'connection-dot downloading';
|
|
2584
2402
|
const progress = this._modelDownloadProgress;
|
|
@@ -2660,9 +2478,6 @@ class AgentGUIClient {
|
|
|
2660
2478
|
setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 5000);
|
|
2661
2479
|
}
|
|
2662
2480
|
|
|
2663
|
-
/**
|
|
2664
|
-
* Update metrics display
|
|
2665
|
-
*/
|
|
2666
2481
|
updateMetrics(metrics) {
|
|
2667
2482
|
const metricsDisplay = document.querySelector('[data-metrics]');
|
|
2668
2483
|
if (metricsDisplay && metrics) {
|
|
@@ -2670,9 +2485,6 @@ class AgentGUIClient {
|
|
|
2670
2485
|
}
|
|
2671
2486
|
}
|
|
2672
2487
|
|
|
2673
|
-
/**
|
|
2674
|
-
* Disable UI controls during execution - prevents double-sends
|
|
2675
|
-
*/
|
|
2676
2488
|
disableControls() {
|
|
2677
2489
|
if (this.ui.sendButton) this.ui.sendButton.disabled = true;
|
|
2678
2490
|
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'DISABLED' });
|
|
@@ -2686,17 +2498,11 @@ class AgentGUIClient {
|
|
|
2686
2498
|
this.updateBusyPromptArea(this.state.currentConversation?.id);
|
|
2687
2499
|
}
|
|
2688
2500
|
|
|
2689
|
-
/**
|
|
2690
|
-
* Toggle theme
|
|
2691
|
-
*/
|
|
2692
2501
|
toggleTheme() {
|
|
2693
2502
|
const isDark = document.documentElement.classList.toggle('dark');
|
|
2694
2503
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
2695
2504
|
}
|
|
2696
2505
|
|
|
2697
|
-
/**
|
|
2698
|
-
* Create a new empty conversation
|
|
2699
|
-
*/
|
|
2700
2506
|
async createNewConversation(workingDirectory, title) {
|
|
2701
2507
|
try {
|
|
2702
2508
|
const agentId = this.getEffectiveAgentId();
|
|
@@ -2750,16 +2556,10 @@ class AgentGUIClient {
|
|
|
2750
2556
|
this.conversationCache.delete(conversationId);
|
|
2751
2557
|
}
|
|
2752
2558
|
|
|
2753
|
-
/**
|
|
2754
|
-
* PHASE 2: Create a new load request with lifetime tracking
|
|
2755
|
-
* Assigns unique requestId, tracks in _loadInProgress, returns abort signal
|
|
2756
|
-
* Automatically cancels previous loads to this conversation
|
|
2757
|
-
*/
|
|
2758
2559
|
_makeLoadRequest(conversationId) {
|
|
2759
2560
|
const requestId = ++this._currentRequestId;
|
|
2760
2561
|
const abortController = new AbortController();
|
|
2761
2562
|
|
|
2762
|
-
// Cancel previous request to this conversation
|
|
2763
2563
|
if (this._loadInProgress[conversationId]) {
|
|
2764
2564
|
const prevReq = this._loadInProgress[conversationId];
|
|
2765
2565
|
try {
|
|
@@ -2777,11 +2577,6 @@ class AgentGUIClient {
|
|
|
2777
2577
|
return { requestId, abortController: abortController.signal };
|
|
2778
2578
|
}
|
|
2779
2579
|
|
|
2780
|
-
/**
|
|
2781
|
-
* PHASE 2: Verify request is still current before rendering
|
|
2782
|
-
* Returns true if requestId matches current load for this conversation
|
|
2783
|
-
* Returns false if newer request arrived, or request was cancelled
|
|
2784
|
-
*/
|
|
2785
2580
|
_verifyRequestId(conversationId, requestId) {
|
|
2786
2581
|
const current = this._loadInProgress[conversationId];
|
|
2787
2582
|
if (!current) return false;
|
|
@@ -2789,9 +2584,6 @@ class AgentGUIClient {
|
|
|
2789
2584
|
return true;
|
|
2790
2585
|
}
|
|
2791
2586
|
|
|
2792
|
-
/**
|
|
2793
|
-
* PHASE 2: Complete/cleanup a load request
|
|
2794
|
-
*/
|
|
2795
2587
|
_completeLoadRequest(conversationId, requestId) {
|
|
2796
2588
|
const req = this._loadInProgress[conversationId];
|
|
2797
2589
|
if (req && req.requestId === requestId) {
|
|
@@ -2820,8 +2612,6 @@ class AgentGUIClient {
|
|
|
2820
2612
|
if (this.ui.messageInput) {
|
|
2821
2613
|
this.ui.messageInput.value = '';
|
|
2822
2614
|
this.ui.messageInput.style.height = 'auto';
|
|
2823
|
-
// Note: prompt disabled state will be set immutably based on shouldResumeStreaming
|
|
2824
|
-
// after conversation data loads, don't set here
|
|
2825
2615
|
}
|
|
2826
2616
|
|
|
2827
2617
|
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
@@ -2875,7 +2665,6 @@ class AgentGUIClient {
|
|
|
2875
2665
|
|
|
2876
2666
|
if (this._lazyObserver) { this._lazyObserver.disconnect(); this._lazyObserver = null; }
|
|
2877
2667
|
this._showSkeletonLoading(conversationId);
|
|
2878
|
-
// Yield to let skeleton paint before blocking on RPC
|
|
2879
2668
|
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
2880
2669
|
|
|
2881
2670
|
let fullData;
|
|
@@ -2933,7 +2722,6 @@ class AgentGUIClient {
|
|
|
2933
2722
|
const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this._convIsStreaming(conversationId);
|
|
2934
2723
|
this.applyAgentAndModelSelection(conversation, hasActivity);
|
|
2935
2724
|
|
|
2936
|
-
// Parse chunk data and fetch queue in parallel
|
|
2937
2725
|
const queuePromise = window.wsClient.rpc('q.ls', { id: conversationId }).catch(() => ({ queue: [] }));
|
|
2938
2726
|
const chunks = (rawChunks || []).map(chunk => ({
|
|
2939
2727
|
...chunk,
|
|
@@ -3002,7 +2790,6 @@ class AgentGUIClient {
|
|
|
3002
2790
|
if (chunks.length > 0) {
|
|
3003
2791
|
const activeSessionId = (shouldResumeStreaming && latestSession) ? latestSession.id : null;
|
|
3004
2792
|
performance.mark(`conv-render-start:${conversationId}`);
|
|
3005
|
-
// Yield before heavy render to let header paint
|
|
3006
2793
|
await new Promise(r => requestAnimationFrame(r));
|
|
3007
2794
|
if (!convSignal.aborted) this._renderConversationContent(messagesEl, chunks, userMessages, activeSessionId);
|
|
3008
2795
|
performance.mark(`conv-render-complete:${conversationId}`);
|
|
@@ -3043,16 +2830,13 @@ class AgentGUIClient {
|
|
|
3043
2830
|
|
|
3044
2831
|
this.updateUrlForConversation(conversationId, latestSession.id);
|
|
3045
2832
|
|
|
3046
|
-
// Flush any blocks accumulated in the background cache while this conv wasn't active
|
|
3047
2833
|
this._flushBgCache(conversationId, latestSession.id);
|
|
3048
2834
|
|
|
3049
|
-
// IMMUTABLE: Prompt remains enabled - syncPromptState will set correct state
|
|
3050
2835
|
this.syncPromptState(conversationId);
|
|
3051
2836
|
} else {
|
|
3052
2837
|
this.syncPromptState(conversationId);
|
|
3053
2838
|
}
|
|
3054
2839
|
|
|
3055
|
-
// Re-enable send button after skeleton loading completes
|
|
3056
2840
|
if (this.ui.sendButton) {
|
|
3057
2841
|
this.ui.sendButton.disabled = false;
|
|
3058
2842
|
}
|
|
@@ -3060,14 +2844,12 @@ class AgentGUIClient {
|
|
|
3060
2844
|
this.restoreScrollPosition(conversationId);
|
|
3061
2845
|
this.setupScrollUpDetection(conversationId);
|
|
3062
2846
|
|
|
3063
|
-
// Fetch and display queue items so queued messages show in yellow blocks, not as user messages
|
|
3064
2847
|
this.fetchAndRenderQueue(conversationId);
|
|
3065
2848
|
|
|
3066
2849
|
}
|
|
3067
2850
|
} catch (error) {
|
|
3068
2851
|
if (error.name === 'AbortError') return;
|
|
3069
2852
|
console.error('Failed to load conversation messages:', error);
|
|
3070
|
-
// Resume from last successful conversation if available, or fall back to any available conversation
|
|
3071
2853
|
const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
|
|
3072
2854
|
if (fallbackConv && fallbackConv !== conversationId) {
|
|
3073
2855
|
this._dbg('Resuming from fallback conversation due to error:', fallbackConv);
|
|
@@ -3263,16 +3045,10 @@ class AgentGUIClient {
|
|
|
3263
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('');
|
|
3264
3046
|
}
|
|
3265
3047
|
|
|
3266
|
-
/**
|
|
3267
|
-
* Escape HTML to prevent XSS
|
|
3268
|
-
*/
|
|
3269
3048
|
escapeHtml(text) {
|
|
3270
3049
|
return window._escHtml(text);
|
|
3271
3050
|
}
|
|
3272
3051
|
|
|
3273
|
-
/**
|
|
3274
|
-
* Show error message
|
|
3275
|
-
*/
|
|
3276
3052
|
showError(message) {
|
|
3277
3053
|
console.error(message);
|
|
3278
3054
|
if (window.UIDialog) {
|
|
@@ -3280,9 +3056,6 @@ class AgentGUIClient {
|
|
|
3280
3056
|
}
|
|
3281
3057
|
}
|
|
3282
3058
|
|
|
3283
|
-
/**
|
|
3284
|
-
* Add event listener
|
|
3285
|
-
*/
|
|
3286
3059
|
on(event, callback) {
|
|
3287
3060
|
if (!this.eventHandlers[event]) {
|
|
3288
3061
|
this.eventHandlers[event] = [];
|
|
@@ -3290,9 +3063,6 @@ class AgentGUIClient {
|
|
|
3290
3063
|
this.eventHandlers[event].push(callback);
|
|
3291
3064
|
}
|
|
3292
3065
|
|
|
3293
|
-
/**
|
|
3294
|
-
* Emit event
|
|
3295
|
-
*/
|
|
3296
3066
|
emit(event, data) {
|
|
3297
3067
|
if (this.eventHandlers[event]) {
|
|
3298
3068
|
this.eventHandlers[event].forEach(callback => {
|
|
@@ -3305,9 +3075,6 @@ class AgentGUIClient {
|
|
|
3305
3075
|
}
|
|
3306
3076
|
}
|
|
3307
3077
|
|
|
3308
|
-
/**
|
|
3309
|
-
* Get current selected agent
|
|
3310
|
-
*/
|
|
3311
3078
|
getEffectiveAgentId() {
|
|
3312
3079
|
return this.ui.cliSelector?.value || null;
|
|
3313
3080
|
}
|
|
@@ -3332,16 +3099,10 @@ class AgentGUIClient {
|
|
|
3332
3099
|
window.wsClient.rpc('conv.upd', { id: convId, agentType: agentId, subAgent: subAgent || undefined, model: model || undefined }).catch(() => {});
|
|
3333
3100
|
}
|
|
3334
3101
|
|
|
3335
|
-
/**
|
|
3336
|
-
* Get current selected model
|
|
3337
|
-
*/
|
|
3338
3102
|
getCurrentModel() {
|
|
3339
3103
|
return this.ui.modelSelector?.value || null;
|
|
3340
3104
|
}
|
|
3341
3105
|
|
|
3342
|
-
/**
|
|
3343
|
-
* Get metrics
|
|
3344
|
-
*/
|
|
3345
3106
|
getMetrics() {
|
|
3346
3107
|
return {
|
|
3347
3108
|
renderer: this.renderer.getMetrics(),
|
|
@@ -3351,9 +3112,6 @@ class AgentGUIClient {
|
|
|
3351
3112
|
};
|
|
3352
3113
|
}
|
|
3353
3114
|
|
|
3354
|
-
/**
|
|
3355
|
-
* Save draft prompt for current conversation
|
|
3356
|
-
*/
|
|
3357
3115
|
saveDraftPrompt() {
|
|
3358
3116
|
const convId = this.state.currentConversation?.id;
|
|
3359
3117
|
if (convId && this.ui.messageInput) {
|
|
@@ -3365,9 +3123,6 @@ class AgentGUIClient {
|
|
|
3365
3123
|
}
|
|
3366
3124
|
}
|
|
3367
3125
|
|
|
3368
|
-
/**
|
|
3369
|
-
* Restore draft prompt for conversation
|
|
3370
|
-
*/
|
|
3371
3126
|
restoreDraftPrompt(conversationId) {
|
|
3372
3127
|
if (!this.ui.messageInput) return;
|
|
3373
3128
|
|
|
@@ -3380,22 +3135,15 @@ class AgentGUIClient {
|
|
|
3380
3135
|
this.ui.messageInput.value = draft;
|
|
3381
3136
|
}
|
|
3382
3137
|
|
|
3383
|
-
/**
|
|
3384
|
-
* Clear draft for conversation
|
|
3385
|
-
*/
|
|
3386
3138
|
clearDraft(conversationId) {
|
|
3387
3139
|
this.draftPrompts.delete(conversationId);
|
|
3388
3140
|
localStorage.removeItem(`draft-${conversationId}`);
|
|
3389
3141
|
}
|
|
3390
3142
|
|
|
3391
|
-
/**
|
|
3392
|
-
* Update send button state based on WebSocket connection
|
|
3393
|
-
*/
|
|
3394
3143
|
updateSendButtonState() {
|
|
3395
3144
|
if (this.ui.sendButton) {
|
|
3396
3145
|
this.ui.sendButton.disabled = !this.wsManager.isConnected;
|
|
3397
3146
|
}
|
|
3398
|
-
// Also disable queue and inject buttons if disconnected
|
|
3399
3147
|
if (this.ui.injectButton && this.ui.injectButton.classList.contains('visible')) {
|
|
3400
3148
|
this.ui.injectButton.disabled = !this.wsManager.isConnected;
|
|
3401
3149
|
}
|
|
@@ -3404,17 +3152,9 @@ class AgentGUIClient {
|
|
|
3404
3152
|
}
|
|
3405
3153
|
}
|
|
3406
3154
|
|
|
3407
|
-
/**
|
|
3408
|
-
* Disable prompt area - NEVER CALLED. Prompt must always be enabled.
|
|
3409
|
-
* Keeping method for backward compatibility but it does nothing.
|
|
3410
|
-
*/
|
|
3411
3155
|
disablePromptArea() {
|
|
3412
|
-
// NEVER disable messageInput - prompt must always be writable
|
|
3413
3156
|
}
|
|
3414
3157
|
|
|
3415
|
-
/**
|
|
3416
|
-
* Enable prompt area (input and inject button) on connect
|
|
3417
|
-
*/
|
|
3418
3158
|
enablePromptArea() {
|
|
3419
3159
|
if (this.ui.messageInput) {
|
|
3420
3160
|
this.ui.messageInput.disabled = false;
|
|
@@ -3423,9 +3163,6 @@ class AgentGUIClient {
|
|
|
3423
3163
|
if (injectBtn) injectBtn.disabled = false;
|
|
3424
3164
|
}
|
|
3425
3165
|
|
|
3426
|
-
/**
|
|
3427
|
-
* Show queue/inject buttons when streaming (busy prompt state)
|
|
3428
|
-
*/
|
|
3429
3166
|
showStreamingPromptButtons() {
|
|
3430
3167
|
if (this.ui.injectButton) {
|
|
3431
3168
|
this.ui.injectButton.classList.add('visible');
|
|
@@ -3437,18 +3174,12 @@ class AgentGUIClient {
|
|
|
3437
3174
|
}
|
|
3438
3175
|
}
|
|
3439
3176
|
|
|
3440
|
-
/**
|
|
3441
|
-
* Ensure prompt area is always enabled and shows queue/inject when agent streaming
|
|
3442
|
-
*/
|
|
3443
3177
|
ensurePromptAreaAlwaysEnabled() {
|
|
3444
3178
|
if (this.ui.messageInput) {
|
|
3445
3179
|
this.ui.messageInput.disabled = false;
|
|
3446
3180
|
}
|
|
3447
3181
|
}
|
|
3448
3182
|
|
|
3449
|
-
/**
|
|
3450
|
-
* Cleanup resources
|
|
3451
|
-
*/
|
|
3452
3183
|
destroy() {
|
|
3453
3184
|
|
|
3454
3185
|
this.renderer.destroy();
|
|
@@ -3462,10 +3193,8 @@ window.__convPerfMetrics = () => {
|
|
|
3462
3193
|
return entries.map(e => ({ name: e.name, ms: Math.round(e.duration) }));
|
|
3463
3194
|
};
|
|
3464
3195
|
|
|
3465
|
-
// Global instance
|
|
3466
3196
|
let agentGUIClient = null;
|
|
3467
3197
|
|
|
3468
|
-
// Initialize on DOM ready
|
|
3469
3198
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
3470
3199
|
try {
|
|
3471
3200
|
agentGUIClient = new AgentGUIClient();
|
|
@@ -3477,7 +3206,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
3477
3206
|
}
|
|
3478
3207
|
});
|
|
3479
3208
|
|
|
3480
|
-
// Export for testing
|
|
3481
3209
|
if (typeof module !== 'undefined' && module.exports) {
|
|
3482
3210
|
module.exports = AgentGUIClient;
|
|
3483
3211
|
}
|