Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 119 additions & 8 deletions chrome-extension/content_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 = `
<textarea
class="float-comment-input"
placeholder="What needs to be fixed?"
<textarea
class="float-comment-input"
placeholder="What needs to be fixed? (paste an image to attach a reference)"
autofocus
></textarea>
<div class="float-comment-attachments"></div>
<div class="float-comment-actions">
<button class="float-comment-cancel">Cancel</button>
<button class="float-comment-submit">Submit (Enter)</button>
Expand Down Expand Up @@ -3278,10 +3378,14 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are

// Focus textarea
setTimeout(() => textarea.focus(), 50);

// Allow pasting reference images from the clipboard
enablePasteImages(commentBox, textarea);

// Handle submit
const handleSubmit = async () => {
const content = textarea.value.trim();
const content = textarea.value.trim()
|| (commentBox.pastedImages?.length ? 'See pasted reference image(s).' : '');
if (!content) return;

const rect = element.getBoundingClientRect();
Expand Down Expand Up @@ -3339,6 +3443,7 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are
hoverOverlay = null;
}

annotation.pastedImages = commentBox.pastedImages || [];
addToQueue(annotation);
exitCommentMode();
};
Expand Down Expand Up @@ -3562,11 +3667,12 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are
commentBox.screenshotData = screenshotData;
commentBox.rectangleData = rectangleData; // Store rectangle data
commentBox.innerHTML = `
<textarea
class="float-comment-input"
placeholder="What needs to be fixed?"
<textarea
class="float-comment-input"
placeholder="What needs to be fixed? (paste an image to attach a reference)"
autofocus
></textarea>
<div class="float-comment-attachments"></div>
<div class="float-comment-actions">
<button class="float-comment-cancel">Cancel</button>
<button class="float-comment-submit">Submit (Enter)</button>
Expand Down Expand Up @@ -3619,10 +3725,14 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are

// Focus textarea
setTimeout(() => textarea.focus(), 50);

// Allow pasting reference images from the clipboard
enablePasteImages(commentBox, textarea);

// Handle submit
const handleSubmit = async () => {
const content = textarea.value.trim();
const content = textarea.value.trim()
|| (commentBox.pastedImages?.length ? 'See pasted reference image(s).' : '');
if (!content) return;

// Get rectangle data from comment box
Expand Down Expand Up @@ -3677,6 +3787,7 @@ JSON stores relative paths like \`./screenshots/file.png\`, but actual files are
exitDrawingMode();
removeDrawingCanvas();

annotation.pastedImages = commentBox.pastedImages || [];
addToQueue(annotation);
removeCommentBox();
};
Expand Down
46 changes: 46 additions & 0 deletions chrome-extension/moat.css
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,52 @@ body.float-drawing-mode * {
border-color: var(--moat-accent);
}

.float-comment-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}

.float-comment-attachments:empty {
display: none;
}

.float-comment-thumb {
position: relative;
width: 56px;
height: 56px;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--moat-border);
}

.float-comment-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

.float-comment-thumb-remove {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: 13px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}

.float-comment-actions {
display: flex;
justify-content: flex-end;
Expand Down
1 change: 1 addition & 0 deletions chrome-extension/rules-templates/bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Files to read:
- `comment`: User's instruction
- `selector`: CSS selector for target element
- `screenshotPath`: Visual context (resolve `./screenshots/` to `.moat/screenshots/`)
- `attachments`: Optional paths to reference images the user pasted from the clipboard — open each one (resolve `./screenshots/` the same way) before implementing, just like the screenshot
- `status`: Current task status ("to do", "doing", "done")

2. **Analyze Dependencies**: Before starting, check if tasks reference each other:
Expand Down
3 changes: 3 additions & 0 deletions chrome-extension/rules-templates/drawbridge-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The `.json` file is the primary source of truth for all task information. You mu
- `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

Expand Down Expand Up @@ -229,6 +230,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. **Screenshot Missing/Inaccessible**:
```
⚠️ Screenshot not found: .moat/screenshots/moat-[id].png
Expand Down
6 changes: 6 additions & 0 deletions chrome-extension/utils/taskStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ function createTaskObject(taskData) {
timestamp: Date.now()
};

// Preserve pasted clipboard reference image paths, if any
if (taskData.attachments?.length) {
task.attachments = taskData.attachments;
}

// Preserve bounding box data for freeform rectangles
if (taskData.boundingBox) {
task.boundingBox = taskData.boundingBox;
Expand Down Expand Up @@ -170,6 +175,7 @@ class TaskStore {
selector: taskData.selector || existingTask.selector,
boundingRect: taskData.boundingRect || existingTask.boundingRect,
screenshotPath: taskData.screenshotPath || existingTask.screenshotPath,
attachments: taskData.attachments || existingTask.attachments,
lastModified: Date.now()
});
console.log(`Updated existing task: ${existingTask.id}`);
Expand Down