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.
@@ -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
  }