agentgui 1.0.210 → 1.0.212

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.
@@ -45,7 +45,6 @@ class AgentGUIClient {
45
45
  agentSelector: null
46
46
  };
47
47
 
48
- // Chunk polling state (must be in constructor so it exists before any event handlers fire)
49
48
  this.chunkPollState = {
50
49
  isPolling: false,
51
50
  lastFetchTimestamp: 0,
@@ -55,6 +54,14 @@ class AgentGUIClient {
55
54
  abortController: null
56
55
  };
57
56
 
57
+ this._pollIntervalByTier = {
58
+ excellent: 100, good: 200, fair: 400, poor: 800, bad: 1500, unknown: 200
59
+ };
60
+
61
+ this._renderedSeqs = new Map();
62
+ this._inflightRequests = new Map();
63
+ this._previousConvAbort = null;
64
+
58
65
  // Router state
59
66
  this.routerState = {
60
67
  currentConversationId: null,
@@ -113,6 +120,7 @@ class AgentGUIClient {
113
120
  this.wsManager.on('connected', () => {
114
121
  console.log('WebSocket connected');
115
122
  this.updateConnectionStatus('connected');
123
+ this._recoverMissedChunks();
116
124
  this.emit('ws:connected');
117
125
  });
118
126
 
@@ -136,10 +144,8 @@ class AgentGUIClient {
136
144
  this.showError('Connection error: ' + (data.error?.message || 'unknown'));
137
145
  });
138
146
 
139
- this.wsManager.on('reconnect_failed', (data) => {
140
- console.error('WebSocket reconnection failed:', data);
141
- this.updateConnectionStatus('error');
142
- this.showError('Failed to reconnect to server after ' + data.attempts + ' attempts');
147
+ this.wsManager.on('latency_update', (data) => {
148
+ this._updateConnectionIndicator(data.quality);
143
149
  });
144
150
  }
145
151
 
@@ -584,10 +590,41 @@ class AgentGUIClient {
584
590
  requestAnimationFrame(() => {
585
591
  this._scrollRafPending = false;
586
592
  const scrollContainer = document.getElementById('output-scroll');
587
- if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
593
+ if (!scrollContainer) return;
594
+ const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
595
+ if (distFromBottom < 150) {
596
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
597
+ this._removeNewContentPill();
598
+ } else {
599
+ this._unseenCount = (this._unseenCount || 0) + 1;
600
+ this._showNewContentPill();
601
+ }
588
602
  });
589
603
  }
590
604
 
605
+ _showNewContentPill() {
606
+ let pill = document.getElementById('new-content-pill');
607
+ const scrollContainer = document.getElementById('output-scroll');
608
+ if (!scrollContainer) return;
609
+ if (!pill) {
610
+ pill = document.createElement('button');
611
+ pill.id = 'new-content-pill';
612
+ pill.className = 'new-content-pill';
613
+ pill.addEventListener('click', () => {
614
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
615
+ this._removeNewContentPill();
616
+ });
617
+ scrollContainer.appendChild(pill);
618
+ }
619
+ pill.textContent = (this._unseenCount || 1) + ' new';
620
+ }
621
+
622
+ _removeNewContentPill() {
623
+ this._unseenCount = 0;
624
+ const pill = document.getElementById('new-content-pill');
625
+ if (pill) pill.remove();
626
+ }
627
+
591
628
  handleStreamingError(data) {
592
629
  console.error('Streaming error:', data);
593
630
 
@@ -688,6 +725,22 @@ class AgentGUIClient {
688
725
  return;
689
726
  }
690
727
 
728
+ if (data.message.role === 'user') {
729
+ const pending = outputEl.querySelector('.message-sending');
730
+ if (pending) {
731
+ pending.id = '';
732
+ pending.setAttribute('data-msg-id', data.message.id);
733
+ pending.classList.remove('message-sending');
734
+ const ts = pending.querySelector('.message-timestamp');
735
+ if (ts) {
736
+ ts.style.opacity = '1';
737
+ ts.textContent = new Date(data.message.created_at).toLocaleString();
738
+ }
739
+ this.emit('message:created', data);
740
+ return;
741
+ }
742
+ }
743
+
691
744
  const messageHtml = `
692
745
  <div class="message message-${data.message.role}" data-msg-id="${data.message.id}">
693
746
  <div class="message-role">${data.message.role.charAt(0).toUpperCase() + data.message.role.slice(1)}</div>
@@ -983,20 +1036,25 @@ class AgentGUIClient {
983
1036
  return;
984
1037
  }
985
1038
 
1039
+ const savedPrompt = prompt;
986
1040
  if (this.ui.messageInput) {
987
1041
  this.ui.messageInput.value = '';
988
1042
  this.ui.messageInput.style.height = 'auto';
989
1043
  }
990
1044
 
1045
+ const pendingId = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
1046
+ this._showOptimisticMessage(pendingId, savedPrompt);
1047
+ this.disableControls();
1048
+
991
1049
  try {
992
1050
  if (this.state.currentConversation?.id) {
993
- await this.streamToConversation(this.state.currentConversation.id, prompt, agentId);
1051
+ await this.streamToConversation(this.state.currentConversation.id, savedPrompt, agentId);
1052
+ this._confirmOptimisticMessage(pendingId);
994
1053
  } else {
995
- this.disableControls();
996
1054
  const response = await fetch(window.__BASE_URL + '/api/conversations', {
997
1055
  method: 'POST',
998
1056
  headers: { 'Content-Type': 'application/json' },
999
- body: JSON.stringify({ agentId, title: prompt.substring(0, 50) })
1057
+ body: JSON.stringify({ agentId, title: savedPrompt.substring(0, 50) })
1000
1058
  });
1001
1059
  const { conversation } = await response.json();
1002
1060
  this.state.currentConversation = conversation;
@@ -1006,15 +1064,124 @@ class AgentGUIClient {
1006
1064
  window.conversationManager.select(conversation.id);
1007
1065
  }
1008
1066
 
1009
- await this.streamToConversation(conversation.id, prompt, agentId);
1067
+ await this.streamToConversation(conversation.id, savedPrompt, agentId);
1068
+ this._confirmOptimisticMessage(pendingId);
1010
1069
  }
1011
1070
  } catch (error) {
1012
1071
  console.error('Execution error:', error);
1013
- this.showError('Failed to start execution: ' + error.message);
1072
+ this._failOptimisticMessage(pendingId, savedPrompt, error.message);
1014
1073
  this.enableControls();
1015
1074
  }
1016
1075
  }
1017
1076
 
1077
+ _showOptimisticMessage(pendingId, content) {
1078
+ const messagesEl = document.querySelector('.conversation-messages');
1079
+ if (!messagesEl) return;
1080
+ const div = document.createElement('div');
1081
+ div.className = 'message message-user message-sending';
1082
+ div.id = pendingId;
1083
+ div.innerHTML = `<div class="message-role">User</div><div class="message-text">${this.escapeHtml(content)}</div><div class="message-timestamp" style="opacity:0.5">Sending...</div>`;
1084
+ messagesEl.appendChild(div);
1085
+ this.scrollToBottom();
1086
+ }
1087
+
1088
+ _confirmOptimisticMessage(pendingId) {
1089
+ const el = document.getElementById(pendingId);
1090
+ if (!el) return;
1091
+ el.classList.remove('message-sending');
1092
+ const ts = el.querySelector('.message-timestamp');
1093
+ if (ts) {
1094
+ ts.style.opacity = '1';
1095
+ ts.textContent = new Date().toLocaleString();
1096
+ }
1097
+ }
1098
+
1099
+ _failOptimisticMessage(pendingId, content, errorMsg) {
1100
+ const el = document.getElementById(pendingId);
1101
+ if (!el) return;
1102
+ el.classList.remove('message-sending');
1103
+ el.classList.add('message-send-failed');
1104
+ const ts = el.querySelector('.message-timestamp');
1105
+ if (ts) {
1106
+ ts.style.opacity = '1';
1107
+ ts.innerHTML = `<span style="color:var(--color-error)">Failed: ${this.escapeHtml(errorMsg)}</span>`;
1108
+ }
1109
+ if (this.ui.messageInput) {
1110
+ this.ui.messageInput.value = content;
1111
+ }
1112
+ }
1113
+
1114
+ async _recoverMissedChunks() {
1115
+ if (!this.state.currentSession?.id) return;
1116
+ if (!this.state.streamingConversations.has(this.state.currentConversation?.id)) return;
1117
+
1118
+ const sessionId = this.state.currentSession.id;
1119
+ const lastSeq = this.wsManager.getLastSeq(sessionId);
1120
+ if (lastSeq < 0) return;
1121
+
1122
+ try {
1123
+ const url = `${window.__BASE_URL}/api/sessions/${sessionId}/chunks?sinceSeq=${lastSeq}`;
1124
+ const resp = await fetch(url);
1125
+ if (!resp.ok) return;
1126
+ const { chunks: rawChunks } = await resp.json();
1127
+ if (!rawChunks || rawChunks.length === 0) return;
1128
+
1129
+ const chunks = rawChunks.map(c => ({
1130
+ ...c,
1131
+ block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
1132
+ })).filter(c => c.block && c.block.type);
1133
+
1134
+ const dedupedChunks = chunks.filter(c => {
1135
+ const seqSet = this._renderedSeqs.get(sessionId);
1136
+ return !seqSet || !seqSet.has(c.sequence);
1137
+ });
1138
+
1139
+ if (dedupedChunks.length > 0) {
1140
+ this.renderChunkBatch(dedupedChunks);
1141
+ }
1142
+ } catch (e) {
1143
+ console.warn('Chunk recovery failed:', e.message);
1144
+ }
1145
+ }
1146
+
1147
+ _dedupedFetch(key, fetchFn) {
1148
+ if (this._inflightRequests.has(key)) {
1149
+ return this._inflightRequests.get(key);
1150
+ }
1151
+ const promise = fetchFn().finally(() => {
1152
+ this._inflightRequests.delete(key);
1153
+ });
1154
+ this._inflightRequests.set(key, promise);
1155
+ return promise;
1156
+ }
1157
+
1158
+ _getAdaptivePollInterval() {
1159
+ const quality = this.wsManager?.latency?.quality || 'unknown';
1160
+ return this._pollIntervalByTier[quality] || 200;
1161
+ }
1162
+
1163
+ _showSkeletonLoading(conversationId) {
1164
+ const outputEl = document.getElementById('output');
1165
+ if (!outputEl) return;
1166
+ const conv = this.state.conversations.find(c => c.id === conversationId);
1167
+ const title = conv?.title || 'Conversation';
1168
+ const wdInfo = conv?.workingDirectory ? ` - ${this.escapeHtml(conv.workingDirectory)}` : '';
1169
+ outputEl.innerHTML = `
1170
+ <div class="conversation-header">
1171
+ <h2>${this.escapeHtml(title)}</h2>
1172
+ <p class="text-secondary">${conv?.agentType || 'unknown'} - ${conv ? new Date(conv.created_at).toLocaleDateString() : ''}${wdInfo}</p>
1173
+ </div>
1174
+ <div class="conversation-messages">
1175
+ <div class="skeleton-loading">
1176
+ <div class="skeleton-block skeleton-pulse" style="height:3rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1177
+ <div class="skeleton-block skeleton-pulse" style="height:6rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1178
+ <div class="skeleton-block skeleton-pulse" style="height:2rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1179
+ <div class="skeleton-block skeleton-pulse" style="height:5rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
1180
+ </div>
1181
+ </div>
1182
+ `;
1183
+ }
1184
+
1018
1185
  async streamToConversation(conversationId, prompt, agentId) {
1019
1186
  try {
1020
1187
  if (this.wsManager.isConnected) {
@@ -1078,6 +1245,12 @@ class AgentGUIClient {
1078
1245
  async fetchChunks(conversationId, since = 0) {
1079
1246
  if (!conversationId) return [];
1080
1247
 
1248
+ if (this.chunkPollState.abortController) {
1249
+ this.chunkPollState.abortController.abort();
1250
+ }
1251
+ this.chunkPollState.abortController = new AbortController();
1252
+ const signal = this.chunkPollState.abortController.signal;
1253
+
1081
1254
  try {
1082
1255
  const params = new URLSearchParams();
1083
1256
  if (since > 0) {
@@ -1085,7 +1258,7 @@ class AgentGUIClient {
1085
1258
  }
1086
1259
 
1087
1260
  const url = `${window.__BASE_URL}/api/conversations/${conversationId}/chunks?${params.toString()}`;
1088
- const response = await fetch(url);
1261
+ const response = await fetch(url, { signal });
1089
1262
 
1090
1263
  if (!response.ok) {
1091
1264
  throw new Error(`HTTP ${response.status}`);
@@ -1096,7 +1269,6 @@ class AgentGUIClient {
1096
1269
  throw new Error('Invalid chunks response');
1097
1270
  }
1098
1271
 
1099
- // Parse JSON data field for each chunk
1100
1272
  const chunks = data.chunks.map(chunk => ({
1101
1273
  ...chunk,
1102
1274
  block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
@@ -1104,6 +1276,7 @@ class AgentGUIClient {
1104
1276
 
1105
1277
  return chunks;
1106
1278
  } catch (error) {
1279
+ if (error.name === 'AbortError') return [];
1107
1280
  console.error('Error fetching chunks:', error);
1108
1281
  throw error;
1109
1282
  }
@@ -1122,7 +1295,7 @@ class AgentGUIClient {
1122
1295
 
1123
1296
  pollState.isPolling = true;
1124
1297
  pollState.lastFetchTimestamp = Date.now();
1125
- pollState.backoffDelay = 150;
1298
+ pollState.backoffDelay = this._getAdaptivePollInterval();
1126
1299
  pollState.sessionCheckCounter = 0;
1127
1300
  pollState.emptyPollCount = 0;
1128
1301
 
@@ -1157,7 +1330,7 @@ class AgentGUIClient {
1157
1330
  const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
1158
1331
 
1159
1332
  if (chunks.length > 0) {
1160
- pollState.backoffDelay = 150;
1333
+ pollState.backoffDelay = this._getAdaptivePollInterval();
1161
1334
  pollState.emptyPollCount = 0;
1162
1335
  const lastChunk = chunks[chunks.length - 1];
1163
1336
  pollState.lastFetchTimestamp = lastChunk.created_at;
@@ -1227,6 +1400,10 @@ class AgentGUIClient {
1227
1400
  const groups = {};
1228
1401
  for (const chunk of chunks) {
1229
1402
  const sid = chunk.sessionId;
1403
+ if (!this._renderedSeqs.has(sid)) this._renderedSeqs.set(sid, new Set());
1404
+ const seqSet = this._renderedSeqs.get(sid);
1405
+ if (chunk.sequence !== undefined && seqSet.has(chunk.sequence)) continue;
1406
+ if (chunk.sequence !== undefined) seqSet.add(chunk.sequence);
1230
1407
  if (!groups[sid]) groups[sid] = [];
1231
1408
  groups[sid].push(chunk);
1232
1409
  }
@@ -1260,40 +1437,42 @@ class AgentGUIClient {
1260
1437
  * Load agents
1261
1438
  */
1262
1439
  async loadAgents() {
1263
- try {
1264
- const response = await fetch(window.__BASE_URL + '/api/agents');
1265
- const { agents } = await response.json();
1266
- this.state.agents = agents;
1267
-
1268
- // Populate agent selector with discovered (available) agents only
1269
- if (this.ui.agentSelector) {
1270
- this.ui.agentSelector.innerHTML = agents
1271
- .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
1272
- .join('');
1273
- }
1274
-
1275
- window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
1440
+ return this._dedupedFetch('loadAgents', async () => {
1441
+ try {
1442
+ const response = await fetch(window.__BASE_URL + '/api/agents');
1443
+ const { agents } = await response.json();
1444
+ this.state.agents = agents;
1445
+
1446
+ if (this.ui.agentSelector) {
1447
+ this.ui.agentSelector.innerHTML = agents
1448
+ .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
1449
+ .join('');
1450
+ }
1276
1451
 
1277
- return agents;
1278
- } catch (error) {
1279
- console.error('Failed to load agents:', error);
1280
- return [];
1281
- }
1452
+ window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
1453
+ return agents;
1454
+ } catch (error) {
1455
+ console.error('Failed to load agents:', error);
1456
+ return [];
1457
+ }
1458
+ });
1282
1459
  }
1283
1460
 
1284
1461
  /**
1285
1462
  * Load conversations
1286
1463
  */
1287
1464
  async loadConversations() {
1288
- try {
1289
- const response = await fetch(window.__BASE_URL + '/api/conversations');
1290
- const { conversations } = await response.json();
1291
- this.state.conversations = conversations;
1292
- return conversations;
1293
- } catch (error) {
1294
- console.error('Failed to load conversations:', error);
1295
- return [];
1296
- }
1465
+ return this._dedupedFetch('loadConversations', async () => {
1466
+ try {
1467
+ const response = await fetch(window.__BASE_URL + '/api/conversations');
1468
+ const { conversations } = await response.json();
1469
+ this.state.conversations = conversations;
1470
+ return conversations;
1471
+ } catch (error) {
1472
+ console.error('Failed to load conversations:', error);
1473
+ return [];
1474
+ }
1475
+ });
1297
1476
  }
1298
1477
 
1299
1478
  /**
@@ -1304,6 +1483,73 @@ class AgentGUIClient {
1304
1483
  this.ui.statusIndicator.dataset.status = status;
1305
1484
  this.ui.statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1306
1485
  }
1486
+ if (status === 'disconnected' || status === 'reconnecting') {
1487
+ this._updateConnectionIndicator(status);
1488
+ } else if (status === 'connected') {
1489
+ this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
1490
+ }
1491
+ }
1492
+
1493
+ _updateConnectionIndicator(quality) {
1494
+ if (this._indicatorDebounce) return;
1495
+ this._indicatorDebounce = true;
1496
+ setTimeout(() => { this._indicatorDebounce = false; }, 1000);
1497
+
1498
+ let indicator = document.getElementById('connection-indicator');
1499
+ if (!indicator) {
1500
+ indicator = document.createElement('div');
1501
+ indicator.id = 'connection-indicator';
1502
+ indicator.className = 'connection-indicator';
1503
+ indicator.innerHTML = '<span class="connection-dot"></span><span class="connection-label"></span>';
1504
+ indicator.addEventListener('click', () => this._toggleConnectionTooltip());
1505
+ const header = document.querySelector('.header-right') || document.querySelector('.app-header');
1506
+ if (header) {
1507
+ header.style.position = 'relative';
1508
+ header.appendChild(indicator);
1509
+ }
1510
+ }
1511
+
1512
+ const dot = indicator.querySelector('.connection-dot');
1513
+ const label = indicator.querySelector('.connection-label');
1514
+ if (!dot || !label) return;
1515
+
1516
+ dot.className = 'connection-dot';
1517
+ if (quality === 'disconnected' || quality === 'reconnecting') {
1518
+ dot.classList.add(quality);
1519
+ label.textContent = quality === 'reconnecting' ? 'Reconnecting...' : 'Disconnected';
1520
+ } else {
1521
+ dot.classList.add(quality);
1522
+ const latency = this.wsManager?.latency;
1523
+ label.textContent = latency?.avg > 0 ? Math.round(latency.avg) + 'ms' : '';
1524
+ }
1525
+ }
1526
+
1527
+ _toggleConnectionTooltip() {
1528
+ let tooltip = document.getElementById('connection-tooltip');
1529
+ if (tooltip) { tooltip.remove(); return; }
1530
+
1531
+ const indicator = document.getElementById('connection-indicator');
1532
+ if (!indicator) return;
1533
+
1534
+ tooltip = document.createElement('div');
1535
+ tooltip.id = 'connection-tooltip';
1536
+ tooltip.className = 'connection-tooltip';
1537
+
1538
+ const latency = this.wsManager?.latency || {};
1539
+ const stats = this.wsManager?.stats || {};
1540
+ const state = this.wsManager?.connectionState || 'unknown';
1541
+
1542
+ tooltip.innerHTML = [
1543
+ `<div>State: ${state}</div>`,
1544
+ `<div>Latency: ${Math.round(latency.avg || 0)}ms</div>`,
1545
+ `<div>Jitter: ${Math.round(latency.jitter || 0)}ms</div>`,
1546
+ `<div>Quality: ${latency.quality || 'unknown'}</div>`,
1547
+ `<div>Reconnects: ${stats.totalReconnects || 0}</div>`,
1548
+ `<div>Uptime: ${stats.lastConnectedTime ? Math.round((Date.now() - stats.lastConnectedTime) / 1000) + 's' : 'N/A'}</div>`
1549
+ ].join('');
1550
+
1551
+ indicator.appendChild(tooltip);
1552
+ setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 5000);
1307
1553
  }
1308
1554
 
1309
1555
  /**
@@ -1404,6 +1650,12 @@ class AgentGUIClient {
1404
1650
 
1405
1651
  async loadConversationMessages(conversationId) {
1406
1652
  try {
1653
+ if (this._previousConvAbort) {
1654
+ this._previousConvAbort.abort();
1655
+ }
1656
+ this._previousConvAbort = new AbortController();
1657
+ const convSignal = this._previousConvAbort.signal;
1658
+
1407
1659
  this.cacheCurrentConversation();
1408
1660
  this.stopChunkPolling();
1409
1661
  var prevId = this.state.currentConversation?.id;
@@ -1420,7 +1672,7 @@ class AgentGUIClient {
1420
1672
  }
1421
1673
 
1422
1674
  const cached = this.conversationCache.get(conversationId);
1423
- if (cached && (Date.now() - cached.timestamp) < 120000) {
1675
+ if (cached && (Date.now() - cached.timestamp) < 300000) {
1424
1676
  const outputEl = document.getElementById('output');
1425
1677
  if (outputEl) {
1426
1678
  outputEl.innerHTML = '';
@@ -1437,7 +1689,9 @@ class AgentGUIClient {
1437
1689
 
1438
1690
  this.conversationCache.delete(conversationId);
1439
1691
 
1440
- const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`);
1692
+ this._showSkeletonLoading(conversationId);
1693
+
1694
+ const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`, { signal: convSignal });
1441
1695
  if (resp.status === 404) {
1442
1696
  console.warn('Conversation no longer exists:', conversationId);
1443
1697
  this.state.currentConversation = null;
@@ -1624,6 +1878,7 @@ class AgentGUIClient {
1624
1878
  this.restoreScrollPosition(conversationId);
1625
1879
  }
1626
1880
  } catch (error) {
1881
+ if (error.name === 'AbortError') return;
1627
1882
  console.error('Failed to load conversation messages:', error);
1628
1883
  this.showError('Failed to load conversation: ' + error.message);
1629
1884
  }