diff --git a/chrome-extension/content_script.js b/chrome-extension/content_script.js index a2a73ed..847c6ae 100644 --- a/chrome-extension/content_script.js +++ b/chrome-extension/content_script.js @@ -353,6 +353,11 @@ screenshotPath: screenshotPath || '' }; + // Pasted clipboard reference images (paths under ./screenshots), if any + if (annotation.attachments?.length) { + taskData.attachments = annotation.attachments; + } + // Add bounding box data for freeform rectangles (preserve all formats) if (annotation.boundingBox && annotation.boundingBox.type === 'freeform') { taskData.boundingBox = annotation.boundingBox; @@ -413,6 +418,85 @@ } } + // Save clipboard-pasted reference images to ./screenshots and return their paths. + async function savePastedImagesToFiles(annotation) { + if (!annotation.pastedImages?.length || !window.directoryHandle) { + return []; + } + const screenshotsDir = await window.directoryHandle.getDirectoryHandle('screenshots', { create: true }); + const paths = []; + for (let i = 0; i < annotation.pastedImages.length; i++) { + const match = /^data:(image\/[\w.+-]+);base64,(.*)$/s.exec(annotation.pastedImages[i]); + if (!match) continue; + const [, mime, base64] = match; + const ext = mime.split('/')[1].replace('jpeg', 'jpg').replace('svg+xml', 'svg'); + const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + const fileName = `${annotation.id}-paste-${i + 1}.${ext}`; + try { + const fileHandle = await screenshotsDir.getFileHandle(fileName, { create: true }); + const writable = await fileHandle.createWritable({ keepExistingData: false }); + await writable.write(new Blob([bytes], { type: mime })); + await writable.close(); + paths.push(`./screenshots/${fileName}`); + } catch (error) { + console.error('Moat: Failed to save pasted image:', error); + } + } + return paths; + } + + // Wire clipboard-image paste onto a comment box's textarea: store image data + // URLs on box.pastedImages and render removable thumbnails. Text paste is + // left untouched (we only intercept when the clipboard carries an image). + function enablePasteImages(box, textarea) { + box.pastedImages = []; + const strip = box.querySelector('.float-comment-attachments'); + + const render = () => { + if (!strip) return; + strip.replaceChildren(); + box.pastedImages.forEach((dataUrl, index) => { + const thumb = document.createElement('div'); + thumb.className = 'float-comment-thumb'; + + const img = document.createElement('img'); + img.src = dataUrl; + thumb.appendChild(img); + + const remove = document.createElement('button'); + remove.type = 'button'; + remove.className = 'float-comment-thumb-remove'; + remove.textContent = '×'; + remove.title = 'Remove'; + remove.addEventListener('click', () => { + box.pastedImages.splice(index, 1); + render(); + }); + thumb.appendChild(remove); + + strip.appendChild(thumb); + }); + }; + + textarea.addEventListener('paste', (e) => { + const items = e.clipboardData?.items; + if (!items) return; + const imageItems = [...items].filter(item => item.type.startsWith('image/')); + if (!imageItems.length) return; // no image on clipboard → normal text paste + e.preventDefault(); + for (const item of imageItems) { + const file = item.getAsFile(); + if (!file) continue; + const reader = new FileReader(); + reader.onload = () => { + box.pastedImages.push(reader.result); + render(); + }; + reader.readAsDataURL(file); + } + }); + } + /** * Capture screenshot using Chrome's native API and crop to specified region * @param {Object} captureArea - { x, y, width, height } in viewport/CSS pixels @@ -1374,6 +1458,7 @@ The \`.json\` file is the primary source of truth for all task information. You - \`selector\`: The precise CSS selector for the target element. - \`title\`, \`boundingRect\`: Context for locating the element. - \`screenshotPath\`: Path to the screenshot showing the user's annotation context. +- \`attachments\`: Optional paths to reference images the user pasted from the clipboard (e.g. a target design/mockup). Open each one before implementing, just like the screenshot. ### Task Dependency Detection @@ -1448,6 +1533,8 @@ Result: Task 1 must complete before Tasks 2 & 3 - Current state vs desired state - Layout and positioning context + **Pasted reference images**: If the task JSON has an \`attachments\` array, read each listed image as well. These are references the user pasted from their clipboard (a target design, an example to match) and must inform the implementation alongside the screenshot. + 3. **Validation Steps**: \`\`\` 📸 Viewing screenshot: ./screenshots/moat-1751940243108-aag80q4av.png @@ -1943,6 +2030,8 @@ Files to read: - **Human-readable**: \`moat-tasks.md\` - Task checklist - **Screenshots**: \`.moat/screenshots/\` - Visual context +**Pasted references**: A task's \`attachments\` array (if present) lists images the user pasted from their clipboard (\`./screenshots/...-paste-N.png\`). Open each one before implementing — they are reference images (a target design/example) and must guide the change alongside the screenshot. + **Important**: Always check \`.moat/\` subdirectory first before checking project root. ## ⚠️ CRITICAL: Status Lifecycle (MUST FOLLOW) @@ -2910,6 +2999,16 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are console.log('📝 Moat: ===== STARTING ANNOTATION PROCESSING ====='); console.log('📝 Moat: Processing annotation:', annotation.elementLabel); console.log('📝 Moat: Annotation ID:', annotation.id); + + // Persist any clipboard-pasted reference images and reference them in the + // task comment, the field /bridge always reads, so the LLM picks them up. + if (annotation.pastedImages?.length) { + const refs = await savePastedImagesToFiles(annotation); + if (refs.length) { + annotation.attachments = refs; + annotation.content = `${annotation.content}\n\nPasted reference image(s):\n${refs.map(p => `- ${p}`).join('\n')}`.trim(); + } + } // Choose save system based on availability (Task 2.8: End-to-end flow) const canUseNew = canUseNewTaskSystem(); @@ -3214,11 +3313,12 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are commentBox.highlightedElement = element; // Store element reference commentBox.highlightOverlay = hoverOverlay; // Store overlay reference for cleanup commentBox.innerHTML = ` - +