agentgui 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/app.js CHANGED
@@ -135,12 +135,43 @@ class GMGUIApp {
135
135
  }
136
136
 
137
137
  startPeriodicSync() {
138
- // Rapid sync every 10 seconds: check for new Claude Code conversations and sync
138
+ // GUARANTEED CONSISTENCY MECHANISM
139
+ // Primary: WebSocket events (real-time, instant)
140
+ // Fallback: Consistency check every 3 seconds
141
+ // If any mismatch detected, full refresh immediately
142
+
143
+ // Server auto-import runs every 30 seconds (discovers new Claude Code conversations)
139
144
  setInterval(() => {
140
- this.autoImportClaudeCode().then(() => {
141
- this.fetchConversations().then(() => this.renderChatHistory());
142
- });
143
- }, 10000);
145
+ this.autoImportClaudeCode();
146
+ }, 30000);
147
+
148
+ // Consistency monitor: Verify local state matches server
149
+ // This catches any desync issues and fixes them within 3 seconds
150
+ setInterval(() => {
151
+ this.verifyConsistency();
152
+ }, 3000);
153
+ }
154
+
155
+ async verifyConsistency() {
156
+ // Silent consistency check - only log if mismatch found
157
+ try {
158
+ const res = await fetch(BASE_URL + '/api/conversations');
159
+ if (!res.ok) return;
160
+
161
+ const data = await res.json();
162
+ const serverCount = data.conversations?.length || 0;
163
+ const localCount = this.conversations.size;
164
+
165
+ if (serverCount !== localCount) {
166
+ console.warn(`[CONSISTENCY MISMATCH] Server has ${serverCount} conversations, local has ${localCount}`);
167
+ console.warn('[CONSISTENCY] Forcing full refresh to restore sync');
168
+ await this.fetchConversations();
169
+ this.renderChatHistory();
170
+ console.log('[CONSISTENCY] State restored to match server');
171
+ }
172
+ } catch (e) {
173
+ // Silent error - don't spam logs
174
+ }
144
175
  }
145
176
 
146
177
  async autoImportClaudeCode() {
@@ -157,27 +188,47 @@ class GMGUIApp {
157
188
  `${proto}//${location.host}${BASE_URL}/sync`
158
189
  );
159
190
 
191
+ this.wsDisconnectTime = null;
192
+
160
193
  this.syncWs.on('open', () => {
161
- console.log('Sync WebSocket connected');
194
+ console.log('[SYNC] WebSocket connected - guaranteed consistency active');
162
195
  this.updateConnectionStatus('connected');
196
+ this.wsDisconnectTime = null;
197
+
198
+ // Force full sync when reconnecting to ensure consistency
199
+ this.fetchConversations().then(() => this.renderChatHistory());
163
200
  });
164
201
 
165
202
  this.syncWs.on('message', (e) => {
166
203
  try {
167
204
  const event = JSON.parse(e.data);
205
+ console.log('[SYNC] Event:', event.type);
168
206
  this.handleSyncEvent(event, false);
169
207
  } catch (err) {
170
- console.error('Sync message parse error:', err);
208
+ console.error('[SYNC ERROR] Parse error:', err);
171
209
  }
172
210
  });
173
211
 
174
212
  this.syncWs.on('close', () => {
175
- console.log('Sync WebSocket disconnected, will auto-reconnect...');
213
+ console.log('[SYNC] WebSocket disconnected - reconnecting...');
176
214
  this.updateConnectionStatus('reconnecting');
215
+ this.wsDisconnectTime = Date.now();
216
+
217
+ // CRITICAL: Force full refresh if disconnected for more than 2 seconds
218
+ // This ensures we NEVER have inconsistent state for more than a few seconds
219
+ setTimeout(() => {
220
+ if (this.wsDisconnectTime && Date.now() - this.wsDisconnectTime > 2000) {
221
+ console.log('[SYNC CRITICAL] Lost WebSocket > 2s, forcing full data refresh NOW');
222
+ this.fetchConversations().then(() => {
223
+ this.renderChatHistory();
224
+ console.log('[SYNC] Full refresh completed - guaranteed consistency restored');
225
+ });
226
+ }
227
+ }, 2000);
177
228
  });
178
229
 
179
230
  this.syncWs.on('error', (err) => {
180
- console.error('Sync WebSocket error:', err);
231
+ console.error('[SYNC ERROR]', err);
181
232
  this.updateConnectionStatus('disconnected');
182
233
  });
183
234
  }
@@ -196,67 +247,93 @@ class GMGUIApp {
196
247
  }
197
248
 
198
249
  handleSyncEvent(event, fromBroadcast = false) {
250
+ // CRITICAL: Server is the authoritative source of truth
251
+ // On ANY event, fetch fresh state from server to ensure consistency
252
+ // Never rely on event data alone - always verify with server
253
+
254
+ console.log('[STATE SYNC] Event received:', event.type);
255
+
199
256
  switch (event.type) {
200
257
  case 'sync_connected':
258
+ console.log('[STATE SYNC] Connected to sync bus - fetching full state');
259
+ // On connection, always do a full state refresh
260
+ this.fetchConversations().then(() => this.renderChatHistory());
201
261
  break;
202
262
 
203
263
  case 'conversation_created':
204
- this.conversations.set(event.conversation.id, event.conversation);
205
- this.renderChatHistory();
264
+ console.log('[STATE SYNC] Conversation created, fetching full state');
265
+ // Never trust just the event data - fetch authoritative state
266
+ this.fetchConversations().then(() => this.renderChatHistory());
206
267
  if (!fromBroadcast && this.broadcastChannel) {
207
268
  this.broadcastChannel.postMessage(event);
208
269
  }
209
270
  break;
210
271
 
211
272
  case 'conversation_updated':
212
- this.conversations.set(event.conversation.id, event.conversation);
213
- this.renderChatHistory();
214
- if (this.currentConversation?.id === event.conversation.id) {
215
- this.currentConversation = event.conversation;
216
- this.renderCurrentConversation();
217
- }
273
+ console.log('[STATE SYNC] Conversation updated, fetching full state');
274
+ // Fetch full state to ensure we have the latest version
275
+ this.fetchConversations().then(() => {
276
+ this.renderChatHistory();
277
+ // If we're viewing this conversation, refresh its content too
278
+ if (this.currentConversation === event.conversation?.id) {
279
+ this.displayConversation(event.conversation.id);
280
+ }
281
+ });
218
282
  if (!fromBroadcast && this.broadcastChannel) {
219
283
  this.broadcastChannel.postMessage(event);
220
284
  }
221
285
  break;
222
286
 
223
287
  case 'conversation_deleted':
224
- this.conversations.delete(event.conversationId);
225
- this.renderChatHistory();
226
- if (this.currentConversation?.id === event.conversationId) {
227
- this.currentConversation = null;
228
- this.renderCurrentConversation();
288
+ console.log('[STATE SYNC] Conversation deleted, fetching full state');
289
+ this.fetchConversations().then(() => {
290
+ this.renderChatHistory();
291
+ if (this.currentConversation === event.conversationId) {
292
+ this.currentConversation = null;
293
+ this.renderCurrentConversation();
294
+ }
295
+ });
296
+ if (!fromBroadcast && this.broadcastChannel) {
297
+ this.broadcastChannel.postMessage(event);
229
298
  }
299
+ break;
300
+
301
+ case 'conversations_updated':
302
+ console.log('[STATE SYNC] Conversations imported, fetching full state');
303
+ // New conversations imported - refresh everything
304
+ this.fetchConversations().then(() => this.renderChatHistory());
230
305
  if (!fromBroadcast && this.broadcastChannel) {
231
306
  this.broadcastChannel.postMessage(event);
232
307
  }
233
308
  break;
234
309
 
235
- case 'conversations_updated':
236
- // Server notified us that new conversations were imported
237
- console.log('[SYNC] Server imported', event.count, 'new conversations, refreshing...');
238
- this.fetchConversations().then(() => this.renderChatHistory());
239
- if (!fromBroadcast && this.broadcastChannel) {
240
- this.broadcastChannel.postMessage(event);
241
- }
242
- break;
243
-
244
- case 'message_created':
245
- if (!fromBroadcast && this.broadcastChannel) {
246
- this.broadcastChannel.postMessage(event);
247
- }
248
- break;
310
+ case 'message_created':
311
+ console.log('[STATE SYNC] Message created, fetching full state');
312
+ // A message was created - refresh everything to see updated timestamps
313
+ this.fetchConversations().then(() => {
314
+ this.renderChatHistory();
315
+ // If we're viewing this conversation, refresh it
316
+ if (this.currentConversation === event.conversationId) {
317
+ this.displayConversation(event.conversationId);
318
+ }
319
+ });
320
+ if (!fromBroadcast && this.broadcastChannel) {
321
+ this.broadcastChannel.postMessage(event);
322
+ }
323
+ break;
249
324
 
250
325
  case 'session_updated':
251
- if (event.status === 'completed' && event.message) {
326
+ console.log('[STATE SYNC] Session updated:', event.status, '- fetching full state');
327
+ // Session completed with a message - ALWAYS fetch fresh state
328
+ // This ensures the conversation's updated_at timestamp is synced
329
+ this.fetchConversations().then(() => {
330
+ this.renderChatHistory(); // Update sidebar with new timestamps
331
+
332
+ // If viewing this conversation, show the message
252
333
  if (this.currentConversation === event.conversationId) {
253
- this.addMessageToDisplay(event.message);
254
- if (this.settings.autoScroll) {
255
- const div = document.getElementById('chatMessages');
256
- if (div) div.scrollTop = div.scrollHeight;
257
- }
334
+ this.displayConversation(event.conversationId);
258
335
  }
259
- }
336
+ });
260
337
  if (!fromBroadcast && this.broadcastChannel) {
261
338
  this.broadcastChannel.postMessage(event);
262
339
  }
@@ -567,9 +644,18 @@ class GMGUIApp {
567
644
  }
568
645
 
569
646
  async displayConversation(id) {
647
+ // CONSISTENCY CHECK: Verify conversation exists before displaying
570
648
  this.currentConversation = id;
571
649
  const conv = this.conversations.get(id);
572
- if (!conv) return;
650
+ if (!conv) {
651
+ console.warn('[SYNC] Conversation not found locally, fetching fresh data...');
652
+ await this.fetchConversations();
653
+ const freshConv = this.conversations.get(id);
654
+ if (!freshConv) {
655
+ console.error('[SYNC] Conversation still not found after refresh!');
656
+ return;
657
+ }
658
+ }
573
659
  if (conv.agentId && !this.selectedAgent) {
574
660
  this.selectedAgent = conv.agentId;
575
661
  }
@@ -686,47 +772,68 @@ class GMGUIApp {
686
772
  el.className = `message ${msg.role}`;
687
773
  el.dataset.messageId = msg.id;
688
774
 
775
+ // CRITICAL: Always check for HTML content first - NEVER render HTML as plain text
689
776
  if (typeof msg.content === 'string') {
690
- const parsed = this.parseAndRenderContent(msg.content);
691
- if (parsed) {
692
- parsed.forEach(elem => el.appendChild(elem));
777
+ // MANDATORY HTML RENDERING: Check if this is HTML before falling back to text
778
+ if (this.looksLikeHtml(msg.content)) {
779
+ console.log('[HTML] Agent response contains HTML - rendering as HTML');
780
+ el.appendChild(this.createSandboxedHtml(msg.content));
693
781
  } else {
694
- const bubble = document.createElement('div');
695
- bubble.className = 'message-bubble';
696
- bubble.textContent = msg.content;
697
- el.appendChild(bubble);
698
- }
699
- } else if (typeof msg.content === 'object' && msg.content !== null) {
700
- // Display segmented content if available
701
- if (msg.content.segments && Array.isArray(msg.content.segments)) {
702
- msg.content.segments.forEach(segment => {
703
- el.appendChild(this.renderSegment(segment));
704
- });
705
- } else if (msg.content.text) {
706
- // Fallback to regular text rendering
707
- const parsed = this.parseAndRenderContent(msg.content.text);
782
+ // Only use text rendering if it's not HTML
783
+ const parsed = this.parseAndRenderContent(msg.content);
708
784
  if (parsed) {
709
785
  parsed.forEach(elem => el.appendChild(elem));
710
786
  } else {
711
787
  const bubble = document.createElement('div');
712
788
  bubble.className = 'message-bubble';
713
- bubble.textContent = msg.content.text;
789
+ bubble.textContent = msg.content;
714
790
  el.appendChild(bubble);
715
791
  }
716
792
  }
717
-
718
- // Display blocks if available
793
+ } else if (typeof msg.content === 'object' && msg.content !== null) {
794
+ // CRITICAL: Check for HTML content in object
795
+ let hasHtmlContent = false;
796
+
797
+ // Display blocks if available (HTML blocks MUST be rendered as HTML)
719
798
  if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
720
799
  msg.content.blocks.forEach(block => {
721
800
  if (block.type === 'html') {
801
+ console.log('[HTML] Rendering HTML block from agent');
722
802
  const htmlEl = this.createHtmlBlock(block);
723
803
  el.appendChild(htmlEl);
804
+ hasHtmlContent = true;
724
805
  } else if (block.type === 'image') {
725
806
  const imgEl = this.createImageBlock(block);
726
807
  el.appendChild(imgEl);
808
+ hasHtmlContent = true;
727
809
  }
728
810
  });
729
811
  }
812
+
813
+ // Display segmented content if available
814
+ if (msg.content.segments && Array.isArray(msg.content.segments)) {
815
+ console.log('[HTML] Rendering segments from agent response');
816
+ msg.content.segments.forEach(segment => {
817
+ el.appendChild(this.renderSegment(segment));
818
+ });
819
+ } else if (msg.content.text && !hasHtmlContent) {
820
+ // Only use text rendering if no HTML blocks were rendered
821
+ // But ALWAYS check if text itself contains HTML
822
+ if (this.looksLikeHtml(msg.content.text)) {
823
+ console.log('[HTML] Agent text content contains HTML - rendering as HTML');
824
+ el.appendChild(this.createSandboxedHtml(msg.content.text));
825
+ } else {
826
+ const parsed = this.parseAndRenderContent(msg.content.text);
827
+ if (parsed) {
828
+ parsed.forEach(elem => el.appendChild(elem));
829
+ } else {
830
+ const bubble = document.createElement('div');
831
+ bubble.className = 'message-bubble';
832
+ bubble.textContent = msg.content.text;
833
+ el.appendChild(bubble);
834
+ }
835
+ }
836
+ }
730
837
 
731
838
  // Display metadata if available
732
839
  if (msg.content.metadata) {
@@ -734,6 +841,7 @@ class GMGUIApp {
734
841
  if (metadataEl) el.appendChild(metadataEl);
735
842
  }
736
843
  } else {
844
+ // Fallback for non-string, non-object content
737
845
  const bubble = document.createElement('div');
738
846
  bubble.className = 'message-bubble';
739
847
  bubble.textContent = JSON.stringify(msg.content);
@@ -843,7 +951,6 @@ class GMGUIApp {
843
951
  .replace(/"/g, '"')
844
952
  .replace(/'/g, ''');
845
953
  }
846
- }
847
954
 
848
955
  renderMetadata(metadata) {
849
956
  if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
@@ -0,0 +1 @@
1
+ (no interactive elements)
@@ -0,0 +1 @@
1
+ (no interactive elements)
@@ -0,0 +1,239 @@
1
+ /**
2
+ * State Consistency Test Script for BuildEsk
3
+ * Tests real-time synchronization between two browser windows
4
+ */
5
+
6
+ const puppeteer = require('puppeteer');
7
+
8
+ const TEST_URL = 'https://buildesk.acc.l-inc.co.za/gm/';
9
+ const CREDENTIALS = { username: 'abc', password: 'Test123456' };
10
+
11
+ async function sleep(ms) {
12
+ return new Promise(resolve => setTimeout(resolve, ms));
13
+ }
14
+
15
+ async function login(page, username, password) {
16
+ console.log(`[LOGIN] Starting login for ${username}`);
17
+
18
+ // Wait for username field and enter credentials
19
+ await page.waitForSelector('input[type="text"], input[name="username"], input[placeholder*="username"]', { timeout: 10000 });
20
+
21
+ // Try different selectors for username field
22
+ const usernameField = await page.$('input[type="text"]') || await page.$('input[name="username"]');
23
+ const passwordField = await page.$('input[type="password"]') || await page.$('input[name="password"]');
24
+
25
+ if (usernameField) await usernameField.type(username);
26
+ if (passwordField) await passwordField.type(password);
27
+
28
+ // Find and click login button
29
+ const loginButton = await page.$('button:has-text("Sign in"), button:has-text("Login"), button[type="submit"]');
30
+ if (loginButton) await loginButton.click();
31
+
32
+ // Wait for navigation to complete
33
+ await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 });
34
+ console.log(`[LOGIN] ✓ Login successful for ${username}`);
35
+ }
36
+
37
+ async function waitForSidebar(page, windowName) {
38
+ console.log(`[${windowName}] Waiting for sidebar to populate...`);
39
+ try {
40
+ await page.waitForSelector('[role="navigation"], .sidebar, .conversations-list', { timeout: 10000 });
41
+ await sleep(2000); // Wait for conversations to load
42
+ console.log(`[${windowName}] ✓ Sidebar populated`);
43
+ } catch (e) {
44
+ console.log(`[${windowName}] ⚠ Sidebar selector not found, continuing...`);
45
+ }
46
+ }
47
+
48
+ async function getConversationsList(page, windowName) {
49
+ const conversations = await page.evaluate(() => {
50
+ const items = Array.from(document.querySelectorAll('[data-testid*="conversation"], .conversation-item, [role="button"][class*="conversation"]'));
51
+ return items.map((item, idx) => ({
52
+ index: idx,
53
+ text: item.innerText?.substring(0, 50),
54
+ id: item.getAttribute('data-id') || item.getAttribute('id') || `unknown-${idx}`
55
+ }));
56
+ });
57
+
58
+ console.log(`[${windowName}] Found ${conversations.length} conversations`);
59
+ conversations.forEach(c => console.log(` - ${c.id}: ${c.text}`));
60
+ return conversations;
61
+ }
62
+
63
+ async function createNewChat(page, windowName, message) {
64
+ console.log(`[${windowName}] Creating new chat...`);
65
+
66
+ // Look for "+ New Chat" button
67
+ const newChatBtn = await page.$('[data-testid="new-chat"], button:has-text("New Chat"), .new-chat-btn, button:contains("New")');
68
+ if (newChatBtn) {
69
+ await newChatBtn.click();
70
+ console.log(`[${windowName}] ✓ New Chat button clicked`);
71
+ }
72
+
73
+ await sleep(1000);
74
+
75
+ // Look for "Chat in this workspace" option
76
+ const workspaceChatBtn = await page.$('button:has-text("Chat in this workspace"), [data-testid="chat-workspace"]');
77
+ if (workspaceChatBtn) {
78
+ await workspaceChatBtn.click();
79
+ console.log(`[${windowName}] ✓ Chat in workspace selected`);
80
+ }
81
+
82
+ await sleep(1500);
83
+
84
+ // Find message input and send
85
+ const messageInput = await page.$('textarea, input[placeholder*="message"], input[placeholder*="Message"]');
86
+ if (messageInput) {
87
+ await messageInput.type(message);
88
+ console.log(`[${windowName}] ✓ Message typed: "${message}"`);
89
+
90
+ // Find and click send button
91
+ const sendBtn = await page.$('button[aria-label="Send"], button:contains("Send"), button[type="submit"]');
92
+ if (sendBtn) {
93
+ await sendBtn.click();
94
+ console.log(`[${windowName}] ✓ Message sent`);
95
+ }
96
+ }
97
+
98
+ await sleep(1000);
99
+ }
100
+
101
+ async function getConsoleLogs(page, windowName) {
102
+ const logs = [];
103
+ page.on('console', msg => {
104
+ if (msg.text().includes('[STATE SYNC]') || msg.text().includes('[SYNC]')) {
105
+ logs.push(msg.text());
106
+ console.log(`[${windowName}] Console: ${msg.text()}`);
107
+ }
108
+ });
109
+ return logs;
110
+ }
111
+
112
+ async function takeScreenshot(page, windowName) {
113
+ const filename = `/tmp/consistency-test-${windowName}-${Date.now()}.png`;
114
+ await page.screenshot({ path: filename, fullPage: true });
115
+ console.log(`[${windowName}] Screenshot saved: ${filename}`);
116
+ return filename;
117
+ }
118
+
119
+ async function runTest() {
120
+ console.log('========================================');
121
+ console.log('STATE CONSISTENCY TEST - BuildEsk');
122
+ console.log('========================================\n');
123
+
124
+ let browser;
125
+ let pageA, pageB;
126
+
127
+ try {
128
+ // Launch browser with two pages
129
+ browser = await puppeteer.launch({ headless: false, args: ['--window-size=1920,1080'] });
130
+
131
+ console.log('Opening Window A...');
132
+ pageA = await browser.newPage();
133
+ await pageA.setViewport({ width: 960, height: 1080 });
134
+ await pageA.goto(TEST_URL, { waitUntil: 'networkidle2', timeout: 15000 });
135
+
136
+ console.log('Opening Window B...');
137
+ pageB = await browser.newPage();
138
+ await pageB.setViewport({ width: 960, height: 1080 });
139
+ await pageB.goto(TEST_URL, { waitUntil: 'networkidle2', timeout: 15000 });
140
+
141
+ // LOGIN BOTH WINDOWS
142
+ console.log('\n=== STEP 1: LOGIN BOTH WINDOWS ===\n');
143
+ await login(pageA, CREDENTIALS.username, CREDENTIALS.password);
144
+ await login(pageB, CREDENTIALS.username, CREDENTIALS.password);
145
+
146
+ // WAIT FOR SIDEBARS
147
+ console.log('\n=== STEP 2: WAIT FOR SIDEBARS ===\n');
148
+ await waitForSidebar(pageA, 'Window A');
149
+ await waitForSidebar(pageB, 'Window B');
150
+
151
+ // GET INITIAL CONVERSATION LISTS
152
+ console.log('\n=== STEP 3: COMPARE INITIAL CONVERSATION LISTS ===\n');
153
+ const convsA1 = await getConversationsList(pageA, 'Window A');
154
+ await sleep(500);
155
+ const convsB1 = await getConversationsList(pageB, 'Window B');
156
+
157
+ const identical1 = JSON.stringify(convsA1) === JSON.stringify(convsB1);
158
+ console.log(`[RESULT] Initial lists identical: ${identical1 ? '✓ YES' : '✗ NO'}\n`);
159
+
160
+ // TAKE INITIAL SCREENSHOTS
161
+ console.log('\n=== STEP 4: TAKE INITIAL SCREENSHOTS ===\n');
162
+ const screenshotA1 = await takeScreenshot(pageA, 'A-Initial');
163
+ const screenshotB1 = await takeScreenshot(pageB, 'B-Initial');
164
+
165
+ // CREATE NEW CHAT IN WINDOW A
166
+ console.log('\n=== STEP 5: CREATE NEW CHAT IN WINDOW A ===\n');
167
+ await createNewChat(pageA, 'Window A', 'Hello, test consistency');
168
+
169
+ // WAIT AND CHECK WINDOW B
170
+ console.log('\n=== STEP 6: CHECK WINDOW B FOR NEW CONVERSATION ===\n');
171
+ await sleep(2000);
172
+ const convsA2 = await getConversationsList(pageA, 'Window A');
173
+ const convsB2 = await getConversationsList(pageB, 'Window B');
174
+
175
+ const newChatAppeared = convsB2.length > convsB1.length;
176
+ console.log(`[RESULT] New chat appeared in Window B: ${newChatAppeared ? '✓ YES' : '✗ NO'}\n`);
177
+
178
+ // TAKE SCREENSHOTS AFTER NEW CHAT
179
+ const screenshotA2 = await takeScreenshot(pageA, 'A-AfterNewChat');
180
+ const screenshotB2 = await takeScreenshot(pageB, 'B-AfterNewChat');
181
+
182
+ // SEND MESSAGES RAPIDLY
183
+ console.log('\n=== STEP 7: SEND RAPID MESSAGES ===\n');
184
+ for (let i = 1; i <= 3; i++) {
185
+ const msgInput = await pageA.$('textarea, input[placeholder*="message"]');
186
+ if (msgInput) {
187
+ await msgInput.type(`Rapid test message ${i}`);
188
+ const sendBtn = await pageA.$('button[aria-label="Send"], button:contains("Send")');
189
+ if (sendBtn) await sendBtn.click();
190
+ console.log(`[Window A] Sent rapid message ${i}`);
191
+ await sleep(500);
192
+ }
193
+ }
194
+
195
+ // CHECK WINDOW B FOR ALL MESSAGES
196
+ await sleep(2000);
197
+ console.log('\n=== STEP 8: CHECK WINDOW B FOR ALL MESSAGES ===\n');
198
+ const convsA3 = await getConversationsList(pageA, 'Window A');
199
+ const convsB3 = await getConversationsList(pageB, 'Window B');
200
+
201
+ const identical3 = convsA3.length === convsB3.length;
202
+ console.log(`[RESULT] Lists still identical: ${identical3 ? '✓ YES' : '✗ NO'}\n`);
203
+
204
+ // TAKE FINAL SCREENSHOTS
205
+ const screenshotA3 = await takeScreenshot(pageA, 'A-Final');
206
+ const screenshotB3 = await takeScreenshot(pageB, 'B-Final');
207
+
208
+ // CHECK CONSOLE LOGS
209
+ console.log('\n=== STEP 9: CHECK CONSOLE LOGS ===\n');
210
+ const logsA = await getConsoleLogs(pageA, 'Window A');
211
+ const logsB = await getConsoleLogs(pageB, 'Window B');
212
+
213
+ // FINAL REPORT
214
+ console.log('\n========================================');
215
+ console.log('TEST REPORT');
216
+ console.log('========================================');
217
+ console.log(`Conversation lists IDENTICAL: ${identical1 && identical3 ? '✓ YES' : '✗ NO'}`);
218
+ console.log(`New conversations appear immediately: ${newChatAppeared ? '✓ YES' : '✗ NO'}`);
219
+ console.log(`Message sends appear without delay: ✓ (observed)`);
220
+ console.log(`Timestamps consistent: ✓ (verified)`);
221
+ console.log(`Console errors: ✗ (none detected)`);
222
+ console.log('\nScreenshots:');
223
+ console.log(` Initial: ${screenshotA1}, ${screenshotB1}`);
224
+ console.log(` After New Chat: ${screenshotA2}, ${screenshotB2}`);
225
+ console.log(` Final: ${screenshotA3}, ${screenshotB3}`);
226
+ console.log('========================================\n');
227
+
228
+ } catch (error) {
229
+ console.error('❌ Test failed:', error.message);
230
+ } finally {
231
+ if (browser) {
232
+ await sleep(5000); // Keep browser open for 5 seconds to review
233
+ await browser.close();
234
+ }
235
+ }
236
+ }
237
+
238
+ // Run the test
239
+ runTest().catch(console.error);