agentgui 1.0.29 → 1.0.31
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/DELIVERABLES.txt +212 -0
- package/STATE_CONSISTENCY_TEST_INDEX.md +268 -0
- package/STATE_CONSISTENCY_TEST_REPORT.md +256 -0
- package/TEST_README.md +205 -0
- package/TEST_SUMMARY.md +159 -0
- package/package.json +1 -1
- package/static/app.js +57 -21
- package/static/index.html +0 -7
- package/test-artifacts/01-window-a-initial.png +0 -0
- package/test-artifacts/01-window-b-initial.png +0 -0
- package/test-artifacts/02-window-a-after-send.png +0 -0
- package/test-artifacts/02-window-b-after-send.png +0 -0
- package/test-artifacts/snapshot-a-1.txt +1 -0
- package/test-artifacts/snapshot-b-1.txt +1 -0
- package/test-state-consistency.cjs +239 -0
package/static/app.js
CHANGED
|
@@ -760,7 +760,21 @@ class GMGUIApp {
|
|
|
760
760
|
wrap.className = 'html-block rendered-html';
|
|
761
761
|
const content = document.createElement('div');
|
|
762
762
|
content.className = 'html-content';
|
|
763
|
-
|
|
763
|
+
|
|
764
|
+
// CRITICAL: Ensure RippleUI styles are available for agent HTML
|
|
765
|
+
// Agent responses use RippleUI/Tailwind classes, so wrap in a context that has those styles
|
|
766
|
+
let enhancedHtml = rawHtml;
|
|
767
|
+
|
|
768
|
+
// If HTML doesn't already have the RippleUI wrapper classes, add them
|
|
769
|
+
if (!rawHtml.includes('space-y-4') && !rawHtml.includes('card') && !rawHtml.includes('alert')) {
|
|
770
|
+
// Wrap in RippleUI container if agent didn't already wrap it
|
|
771
|
+
enhancedHtml = `<div class="space-y-4 p-6 max-w-4xl">${rawHtml}</div>`;
|
|
772
|
+
console.log('[HTML] Wrapped agent HTML in RippleUI container for styling');
|
|
773
|
+
} else {
|
|
774
|
+
console.log('[HTML] Agent HTML already has RippleUI classes');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
content.innerHTML = this.sanitizeHtml(enhancedHtml);
|
|
764
778
|
wrap.appendChild(content);
|
|
765
779
|
return wrap;
|
|
766
780
|
}
|
|
@@ -772,47 +786,68 @@ class GMGUIApp {
|
|
|
772
786
|
el.className = `message ${msg.role}`;
|
|
773
787
|
el.dataset.messageId = msg.id;
|
|
774
788
|
|
|
789
|
+
// CRITICAL: Always check for HTML content first - NEVER render HTML as plain text
|
|
775
790
|
if (typeof msg.content === 'string') {
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
791
|
+
// MANDATORY HTML RENDERING: Check if this is HTML before falling back to text
|
|
792
|
+
if (this.looksLikeHtml(msg.content)) {
|
|
793
|
+
console.log('[HTML] Agent response contains HTML - rendering as HTML');
|
|
794
|
+
el.appendChild(this.createSandboxedHtml(msg.content));
|
|
779
795
|
} else {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
bubble.textContent = msg.content;
|
|
783
|
-
el.appendChild(bubble);
|
|
784
|
-
}
|
|
785
|
-
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
786
|
-
// Display segmented content if available
|
|
787
|
-
if (msg.content.segments && Array.isArray(msg.content.segments)) {
|
|
788
|
-
msg.content.segments.forEach(segment => {
|
|
789
|
-
el.appendChild(this.renderSegment(segment));
|
|
790
|
-
});
|
|
791
|
-
} else if (msg.content.text) {
|
|
792
|
-
// Fallback to regular text rendering
|
|
793
|
-
const parsed = this.parseAndRenderContent(msg.content.text);
|
|
796
|
+
// Only use text rendering if it's not HTML
|
|
797
|
+
const parsed = this.parseAndRenderContent(msg.content);
|
|
794
798
|
if (parsed) {
|
|
795
799
|
parsed.forEach(elem => el.appendChild(elem));
|
|
796
800
|
} else {
|
|
797
801
|
const bubble = document.createElement('div');
|
|
798
802
|
bubble.className = 'message-bubble';
|
|
799
|
-
bubble.textContent = msg.content
|
|
803
|
+
bubble.textContent = msg.content;
|
|
800
804
|
el.appendChild(bubble);
|
|
801
805
|
}
|
|
802
806
|
}
|
|
803
|
-
|
|
804
|
-
//
|
|
807
|
+
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
808
|
+
// CRITICAL: Check for HTML content in object
|
|
809
|
+
let hasHtmlContent = false;
|
|
810
|
+
|
|
811
|
+
// Display blocks if available (HTML blocks MUST be rendered as HTML)
|
|
805
812
|
if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
|
|
806
813
|
msg.content.blocks.forEach(block => {
|
|
807
814
|
if (block.type === 'html') {
|
|
815
|
+
console.log('[HTML] Rendering HTML block from agent');
|
|
808
816
|
const htmlEl = this.createHtmlBlock(block);
|
|
809
817
|
el.appendChild(htmlEl);
|
|
818
|
+
hasHtmlContent = true;
|
|
810
819
|
} else if (block.type === 'image') {
|
|
811
820
|
const imgEl = this.createImageBlock(block);
|
|
812
821
|
el.appendChild(imgEl);
|
|
822
|
+
hasHtmlContent = true;
|
|
813
823
|
}
|
|
814
824
|
});
|
|
815
825
|
}
|
|
826
|
+
|
|
827
|
+
// Display segmented content if available
|
|
828
|
+
if (msg.content.segments && Array.isArray(msg.content.segments)) {
|
|
829
|
+
console.log('[HTML] Rendering segments from agent response');
|
|
830
|
+
msg.content.segments.forEach(segment => {
|
|
831
|
+
el.appendChild(this.renderSegment(segment));
|
|
832
|
+
});
|
|
833
|
+
} else if (msg.content.text && !hasHtmlContent) {
|
|
834
|
+
// Only use text rendering if no HTML blocks were rendered
|
|
835
|
+
// But ALWAYS check if text itself contains HTML
|
|
836
|
+
if (this.looksLikeHtml(msg.content.text)) {
|
|
837
|
+
console.log('[HTML] Agent text content contains HTML - rendering as HTML');
|
|
838
|
+
el.appendChild(this.createSandboxedHtml(msg.content.text));
|
|
839
|
+
} else {
|
|
840
|
+
const parsed = this.parseAndRenderContent(msg.content.text);
|
|
841
|
+
if (parsed) {
|
|
842
|
+
parsed.forEach(elem => el.appendChild(elem));
|
|
843
|
+
} else {
|
|
844
|
+
const bubble = document.createElement('div');
|
|
845
|
+
bubble.className = 'message-bubble';
|
|
846
|
+
bubble.textContent = msg.content.text;
|
|
847
|
+
el.appendChild(bubble);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
816
851
|
|
|
817
852
|
// Display metadata if available
|
|
818
853
|
if (msg.content.metadata) {
|
|
@@ -820,6 +855,7 @@ class GMGUIApp {
|
|
|
820
855
|
if (metadataEl) el.appendChild(metadataEl);
|
|
821
856
|
}
|
|
822
857
|
} else {
|
|
858
|
+
// Fallback for non-string, non-object content
|
|
823
859
|
const bubble = document.createElement('div');
|
|
824
860
|
bubble.className = 'message-bubble';
|
|
825
861
|
bubble.textContent = JSON.stringify(msg.content);
|
package/static/index.html
CHANGED
|
@@ -130,13 +130,6 @@
|
|
|
130
130
|
<div class="chat-option-desc">Contextualize chat to a specific folder</div>
|
|
131
131
|
</div>
|
|
132
132
|
</button>
|
|
133
|
-
<button class="chat-option-btn" onclick="importClaudeCodeConversations()">
|
|
134
|
-
<span class="chat-option-icon">📥</span>
|
|
135
|
-
<div class="chat-option-content">
|
|
136
|
-
<div class="chat-option-title">Import Claude Code conversations</div>
|
|
137
|
-
<div class="chat-option-desc">Load existing conversations from Claude Code</div>
|
|
138
|
-
</div>
|
|
139
|
-
</button>
|
|
140
133
|
</div>
|
|
141
134
|
</div>
|
|
142
135
|
</div>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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);
|