debug wip

This commit is contained in:
Guilhem
2026-01-14 11:06:06 +00:00
parent e3ba31da51
commit 2beeb998dd
3 changed files with 454 additions and 25 deletions
@@ -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()
}