mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
debug wip
This commit is contained in:
@@ -173,6 +173,8 @@
|
||||
}
|
||||
|
||||
let iframeLoaded = $state(false) // @hmr:keep
|
||||
let lastFilesSentHash = $state(0)
|
||||
let sendFilesTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function populateFiles() {
|
||||
setFilesInIframe(initFiles)
|
||||
@@ -181,20 +183,46 @@
|
||||
const files = Object.fromEntries(
|
||||
Object.entries(newFiles).filter(([path, _]) => !path.endsWith('/'))
|
||||
)
|
||||
console.log('[SEND] Sending files to iframe', {
|
||||
fileCount: Object.keys(files).length,
|
||||
paths: Object.keys(files),
|
||||
fileSizes: Object.fromEntries(
|
||||
Object.entries(files).map(([path, content]) => [path, content.length])
|
||||
)
|
||||
})
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'setFiles',
|
||||
files: files
|
||||
},
|
||||
'*'
|
||||
|
||||
// Calculate hash to detect if content actually changed
|
||||
const contentHash = JSON.stringify(
|
||||
Object.entries(files)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([path, content]) => [path, content.length, content.slice(0, 100)])
|
||||
)
|
||||
.split('')
|
||||
.reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)
|
||||
|
||||
// Clear any pending send
|
||||
if (sendFilesTimeout) {
|
||||
clearTimeout(sendFilesTimeout)
|
||||
}
|
||||
|
||||
// Debounce: only send after 50ms of no changes
|
||||
sendFilesTimeout = setTimeout(() => {
|
||||
if (contentHash === lastFilesSentHash) {
|
||||
console.log('[SEND] Skipping duplicate send (hash:', contentHash, ')')
|
||||
return
|
||||
}
|
||||
|
||||
lastFilesSentHash = contentHash
|
||||
console.log('[SEND] Sending files to iframe', {
|
||||
fileCount: Object.keys(files).length,
|
||||
hash: contentHash,
|
||||
paths: Object.keys(files),
|
||||
fileSizes: Object.fromEntries(
|
||||
Object.entries(files).map(([path, content]) => [path, content.length])
|
||||
)
|
||||
})
|
||||
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'setFiles',
|
||||
files: files
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function populateRunnables() {
|
||||
@@ -310,15 +338,15 @@
|
||||
files = {}
|
||||
}
|
||||
files[path] = content
|
||||
console.log('[AI] Setting ignoreNextIframeEcho flag')
|
||||
ignoreNextIframeEcho = true
|
||||
setFilesInIframe(files)
|
||||
selectedDocument = path
|
||||
handleSelectFile(path)
|
||||
console.log('[AI] File set complete', {
|
||||
path,
|
||||
currentLength: files[path]?.length,
|
||||
currentHash: files[path]?.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)
|
||||
currentHash: files[path]
|
||||
?.split('')
|
||||
.reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)
|
||||
})
|
||||
return lint()
|
||||
},
|
||||
@@ -599,7 +627,6 @@
|
||||
let inspectorElement: InspectorElementInfo | undefined = $state(undefined)
|
||||
let selectionExcludedFromPrompt: boolean = $state(false)
|
||||
let codeSelection: AppCodeSelectionElement | undefined = $state(undefined)
|
||||
let ignoreNextIframeEcho: boolean = false
|
||||
|
||||
function toggleSelectionExcluded() {
|
||||
selectionExcludedFromPrompt = !selectionExcludedFromPrompt
|
||||
@@ -620,18 +647,11 @@
|
||||
function listener(e: MessageEvent) {
|
||||
if (e.data.type === 'setFiles') {
|
||||
console.log('[IFRAME] Received setFiles message', {
|
||||
fileCount: Object.keys(e.data.files || {}).length,
|
||||
ignoreFlag: ignoreNextIframeEcho
|
||||
fileCount: Object.keys(e.data.files || {}).length
|
||||
})
|
||||
// Prevent corruption from iframe echo after AI sets file
|
||||
if (ignoreNextIframeEcho) {
|
||||
console.log('[IFRAME] Ignoring iframe echo to prevent corruption')
|
||||
ignoreNextIframeEcho = false
|
||||
return
|
||||
}
|
||||
// Normalize Windows-style path separators to Linux-style
|
||||
const normalizedFiles = normalizeFilePaths(e.data.files)
|
||||
// Only mark pending changes if files actually changed (ignore echo from setFilesInIframe)
|
||||
if (!deepEqual(files, normalizedFiles)) {
|
||||
console.log('[IFRAME] Files changed, updating from iframe')
|
||||
files = normalizedFiles
|
||||
@@ -700,6 +720,7 @@
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
console.log('dbg populateFiles')
|
||||
iframe && iframeLoaded && initFiles && populateFiles()
|
||||
})
|
||||
$effect(() => {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Browser console script to debug file corruption
|
||||
*
|
||||
* USAGE:
|
||||
* 1. Open the raw app editor page
|
||||
* 2. Paste this script in the browser console
|
||||
* 3. Ask AI to create/edit a file
|
||||
* 4. Watch the console for detailed logs
|
||||
*/
|
||||
|
||||
(function() {
|
||||
console.log('[CORRUPTION DEBUG] Installing debugging hooks...')
|
||||
|
||||
// Store original postMessage
|
||||
const originalPostMessage = window.postMessage
|
||||
const iframeMessages = []
|
||||
const parentMessages = []
|
||||
|
||||
// Hook window.postMessage to track messages to iframe
|
||||
window.postMessage = function(message, targetOrigin, transfer) {
|
||||
if (message && message.type === 'setFiles') {
|
||||
console.log('[DEBUG] Parent -> Iframe: setFiles', {
|
||||
fileCount: Object.keys(message.files || {}).length,
|
||||
timestamp: Date.now(),
|
||||
files: Object.fromEntries(
|
||||
Object.entries(message.files || {}).map(([path, content]) => [
|
||||
path,
|
||||
{
|
||||
length: content.length,
|
||||
hash: hashString(content),
|
||||
preview: content.substring(0, 100)
|
||||
}
|
||||
])
|
||||
)
|
||||
})
|
||||
iframeMessages.push({
|
||||
type: 'outgoing',
|
||||
timestamp: Date.now(),
|
||||
message: JSON.parse(JSON.stringify(message))
|
||||
})
|
||||
}
|
||||
return originalPostMessage.call(this, message, targetOrigin, transfer)
|
||||
}
|
||||
|
||||
// Hook addEventListener to track messages from iframe
|
||||
const originalAddEventListener = window.addEventListener
|
||||
window.addEventListener = function(type, listener, options) {
|
||||
if (type === 'message') {
|
||||
const wrappedListener = function(event) {
|
||||
if (event.data && event.data.type === 'setFiles') {
|
||||
console.log('[DEBUG] Iframe -> Parent: setFiles', {
|
||||
fileCount: Object.keys(event.data.files || {}).length,
|
||||
timestamp: Date.now(),
|
||||
files: Object.fromEntries(
|
||||
Object.entries(event.data.files || {}).map(([path, content]) => [
|
||||
path,
|
||||
{
|
||||
length: content.length,
|
||||
hash: hashString(content),
|
||||
preview: content.substring(0, 100)
|
||||
}
|
||||
])
|
||||
)
|
||||
})
|
||||
parentMessages.push({
|
||||
type: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
message: JSON.parse(JSON.stringify(event.data))
|
||||
})
|
||||
|
||||
// Check for corruption
|
||||
if (iframeMessages.length > 0) {
|
||||
const lastSent = iframeMessages[iframeMessages.length - 1].message
|
||||
checkForCorruption(lastSent.files, event.data.files)
|
||||
}
|
||||
}
|
||||
return listener.call(this, event)
|
||||
}
|
||||
return originalAddEventListener.call(this, type, wrappedListener, options)
|
||||
}
|
||||
return originalAddEventListener.call(this, type, listener, options)
|
||||
}
|
||||
|
||||
function hashString(str) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash // Convert to 32bit integer
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
function checkForCorruption(sent, received) {
|
||||
console.group('[CORRUPTION CHECK]')
|
||||
|
||||
const sentPaths = Object.keys(sent || {})
|
||||
const receivedPaths = Object.keys(received || {})
|
||||
|
||||
console.log('Files sent:', sentPaths.length)
|
||||
console.log('Files received:', receivedPaths.length)
|
||||
|
||||
// Check each file
|
||||
for (const path of sentPaths) {
|
||||
const sentContent = sent[path]
|
||||
const receivedContent = received[path]
|
||||
|
||||
if (!receivedContent) {
|
||||
console.warn(`❌ File missing in response: ${path}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const sentHash = hashString(sentContent)
|
||||
const receivedHash = hashString(receivedContent)
|
||||
|
||||
if (sentHash !== receivedHash) {
|
||||
console.error(`🔴 CORRUPTION DETECTED: ${path}`)
|
||||
console.log('Sent length:', sentContent.length)
|
||||
console.log('Received length:', receivedContent.length)
|
||||
console.log('Sent hash:', sentHash)
|
||||
console.log('Received hash:', receivedHash)
|
||||
|
||||
// Find first difference
|
||||
const maxLen = Math.min(sentContent.length, receivedContent.length)
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (sentContent[i] !== receivedContent[i]) {
|
||||
console.log(`First diff at position ${i}:`)
|
||||
console.log(` Sent: "${sentContent.substring(Math.max(0, i-20), i+50)}"`)
|
||||
console.log(` Received: "${receivedContent.substring(Math.max(0, i-20), i+50)}"`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Save for inspection
|
||||
window.__corruptedFile = {
|
||||
path,
|
||||
sent: sentContent,
|
||||
received: receivedContent
|
||||
}
|
||||
console.log('💾 Saved to window.__corruptedFile for inspection')
|
||||
} else {
|
||||
console.log(`✅ ${path}: No corruption`)
|
||||
}
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
// Export utilities
|
||||
window.__debugFileCorruption = {
|
||||
iframeMessages,
|
||||
parentMessages,
|
||||
getLastSent: () => iframeMessages[iframeMessages.length - 1],
|
||||
getLastReceived: () => parentMessages[parentMessages.length - 1],
|
||||
compare: (sentIdx = -1, receivedIdx = -1) => {
|
||||
const sent = iframeMessages[sentIdx < 0 ? iframeMessages.length + sentIdx : sentIdx]
|
||||
const received = parentMessages[receivedIdx < 0 ? parentMessages.length + receivedIdx : receivedIdx]
|
||||
|
||||
if (!sent || !received) {
|
||||
console.error('Invalid indices')
|
||||
return
|
||||
}
|
||||
|
||||
checkForCorruption(sent.message.files, received.message.files)
|
||||
},
|
||||
clear: () => {
|
||||
iframeMessages.length = 0
|
||||
parentMessages.length = 0
|
||||
console.log('Cleared message history')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[CORRUPTION DEBUG] Hooks installed! ✅')
|
||||
console.log('Use window.__debugFileCorruption to inspect messages')
|
||||
console.log(' .iframeMessages - Messages sent to iframe')
|
||||
console.log(' .parentMessages - Messages received from iframe')
|
||||
console.log(' .compare() - Compare last sent/received')
|
||||
console.log(' .clear() - Clear history')
|
||||
})()
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Test to investigate file corruption issue when AI sets frontend files
|
||||
*
|
||||
* The issue: When AI calls set_frontend_file with correct content,
|
||||
* the file gets corrupted with duplicated/mangled code.
|
||||
*
|
||||
* Hypothesis: The iframe postMessage roundtrip is corrupting the data.
|
||||
*/
|
||||
|
||||
interface SetFilesMessage {
|
||||
type: 'setFiles'
|
||||
files: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates what happens when AI sets a file
|
||||
*/
|
||||
export function simulateAiSetFile(
|
||||
path: string,
|
||||
content: string,
|
||||
existingFiles: Record<string, string>
|
||||
): {
|
||||
sentToIframe: Record<string, string>
|
||||
log: string[]
|
||||
} {
|
||||
const log: string[] = []
|
||||
|
||||
// Step 1: AI tool calls setFrontendFile
|
||||
log.push(`[AI] Setting file: ${path} (${content.length} chars)`)
|
||||
|
||||
// Step 2: Update files object
|
||||
const files = { ...existingFiles }
|
||||
files[path] = content
|
||||
log.push(`[State] Files object updated`)
|
||||
|
||||
// Step 3: Send to iframe
|
||||
const filesToSend = Object.fromEntries(Object.entries(files).filter(([p, _]) => !p.endsWith('/')))
|
||||
log.push(`[Send] Sending ${Object.keys(filesToSend).length} files to iframe`)
|
||||
|
||||
return {
|
||||
sentToIframe: filesToSend,
|
||||
log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates what the iframe might do when it receives setFiles
|
||||
* This is where corruption might be happening
|
||||
*/
|
||||
export function simulateIframeProcessing(receivedFiles: Record<string, string>): {
|
||||
echoedBack: Record<string, string>
|
||||
log: string[]
|
||||
} {
|
||||
const log: string[] = []
|
||||
log.push(`[Iframe] Received ${Object.keys(receivedFiles).length} files`)
|
||||
|
||||
// The iframe might:
|
||||
// 1. Parse the files
|
||||
// 2. Load into Monaco editor
|
||||
// 3. Format/process somehow
|
||||
// 4. Echo back to parent
|
||||
|
||||
// TODO: This is where we need to investigate what actually happens
|
||||
// For now, just echo back unchanged
|
||||
const echoedBack = { ...receivedFiles }
|
||||
|
||||
log.push(`[Iframe] Echoing back ${Object.keys(echoedBack).length} files`)
|
||||
|
||||
return {
|
||||
echoedBack,
|
||||
log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the full flow
|
||||
*/
|
||||
export function testFileSetFlow() {
|
||||
const originalContent = `<script lang="ts">
|
||||
import { backend } from "./wmill";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Todo {
|
||||
id: number;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
let todos: Todo[] = [];
|
||||
let newTodoText = "";
|
||||
let loading = false;
|
||||
|
||||
onMount(async () => {
|
||||
await loadTodos();
|
||||
});
|
||||
|
||||
async function loadTodos() {
|
||||
try {
|
||||
loading = true;
|
||||
todos = await backend.getTodos();
|
||||
} catch (error) {
|
||||
console.error("Failed to load todos:", error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Todo List</h1>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>`
|
||||
|
||||
const existingFiles = {
|
||||
'/index.tsx': 'export default function App() {}',
|
||||
'/wmill.d.ts': 'export const backend: any'
|
||||
}
|
||||
|
||||
console.log('=== Testing File Corruption Flow ===\n')
|
||||
|
||||
// Step 1: AI sets the file
|
||||
const { sentToIframe, log: aiLog } = simulateAiSetFile(
|
||||
'/App.svelte',
|
||||
originalContent,
|
||||
existingFiles
|
||||
)
|
||||
|
||||
aiLog.forEach((line) => console.log(line))
|
||||
|
||||
console.log('\n--- Content sent to iframe ---')
|
||||
console.log('Path: /App.svelte')
|
||||
console.log('Length:', sentToIframe['/App.svelte']?.length)
|
||||
console.log('First 100 chars:', sentToIframe['/App.svelte']?.substring(0, 100))
|
||||
|
||||
// Step 2: Iframe processes and echoes back
|
||||
const { echoedBack, log: iframeLog } = simulateIframeProcessing(sentToIframe)
|
||||
|
||||
console.log('\n')
|
||||
iframeLog.forEach((line) => console.log(line))
|
||||
|
||||
console.log('\n--- Content echoed back ---')
|
||||
console.log('Path: /App.svelte')
|
||||
console.log('Length:', echoedBack['/App.svelte']?.length)
|
||||
console.log('First 100 chars:', echoedBack['/App.svelte']?.substring(0, 100))
|
||||
|
||||
// Step 3: Compare
|
||||
const original = sentToIframe['/App.svelte']
|
||||
const echoed = echoedBack['/App.svelte']
|
||||
|
||||
console.log('\n=== Comparison ===')
|
||||
console.log('Lengths match:', original.length === echoed.length)
|
||||
console.log('Content match:', original === echoed)
|
||||
|
||||
if (original !== echoed) {
|
||||
console.log('\n🔴 CORRUPTION DETECTED!')
|
||||
console.log('Original length:', original.length)
|
||||
console.log('Echoed length:', echoed.length)
|
||||
|
||||
// Find first difference
|
||||
for (let i = 0; i < Math.min(original.length, echoed.length); i++) {
|
||||
if (original[i] !== echoed[i]) {
|
||||
console.log(`First difference at position ${i}:`)
|
||||
console.log(` Original: "${original.substring(i, i + 50)}"`)
|
||||
console.log(` Echoed: "${echoed.substring(i, i + 50)}"`)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('\n✅ No corruption in test simulation')
|
||||
console.log('(This means corruption happens in actual iframe, not in our data flow)')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to detect corruption patterns in content
|
||||
*/
|
||||
export function detectCorruptionPatterns(content: string): {
|
||||
duplicatedLines: number
|
||||
randomChars: number
|
||||
brokenSyntax: boolean
|
||||
patterns: string[]
|
||||
} {
|
||||
const lines = content.split('\n')
|
||||
const patterns: string[] = []
|
||||
|
||||
// Check for duplicated lines
|
||||
const lineSet = new Set(lines)
|
||||
const duplicatedLines = lines.length - lineSet.size
|
||||
if (duplicatedLines > lines.length * 0.2) {
|
||||
patterns.push(`High duplication: ${duplicatedLines}/${lines.length} lines`)
|
||||
}
|
||||
|
||||
// Check for character corruption (random missing chars)
|
||||
const randomCharMatches = content.match(/\w{2,}[^\w\s]{1}\w{2,}/g) || []
|
||||
const randomChars = randomCharMatches.length
|
||||
if (randomChars > 10) {
|
||||
patterns.push(`Possible char corruption: ${randomChars} instances`)
|
||||
}
|
||||
|
||||
// Check for broken syntax (unmatched brackets)
|
||||
const openBraces = (content.match(/\{/g) || []).length
|
||||
const closeBraces = (content.match(/\}/g) || []).length
|
||||
const openParens = (content.match(/\(/g) || []).length
|
||||
const closeParens = (content.match(/\)/g) || []).length
|
||||
const brokenSyntax =
|
||||
Math.abs(openBraces - closeBraces) > 5 || Math.abs(openParens - closeParens) > 5
|
||||
if (brokenSyntax) {
|
||||
patterns.push(`Unbalanced: {${openBraces}/${closeBraces} }${openParens}/${closeParens}`)
|
||||
}
|
||||
|
||||
return {
|
||||
duplicatedLines,
|
||||
randomChars,
|
||||
brokenSyntax,
|
||||
patterns
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
if (typeof window === 'undefined') {
|
||||
testFileSetFlow()
|
||||
}
|
||||
Reference in New Issue
Block a user