CMMS fork: hand the board photos to the AI bridge

The board photos live in pcb-retrace's IndexedDB, not as CMMS
attachments, so the assistant had no way to see them and burned all its
tool-call rounds looking. cmms-bridge.js now downscales each project
photo (long edge ~1400px, JPEG q0.8 - a phone photo lands under ~300KB)
and sends them alongside the buffer in the cmms-ai-editor-buffer reply
as `images: [{id,name,mime,data_base64}]` (max 6). The CMMS
read_board_image tool surfaces one at a time.
This commit is contained in:
Stuart 2026-09-03 15:23:17 +10:00
commit 40373493b5

View file

@ -160,6 +160,47 @@
return JSON.stringify(view);
}
// The board photos live in pcb-retrace's IndexedDB (imported by the user or
// unpacked from the .pcbretrace.zip), NOT as CMMS attachments, so the
// assistant can't reach them with read_attachment_image. Downscale + JPEG
// each and hand them along with the buffer so a `read_board_image` tool can
// surface them. Long edge ~1400px, quality 0.8 - a phone photo lands well
// under 300KB.
async function encodeImage(blob, maxEdge) {
const bmp = await createImageBitmap(blob);
const s = Math.min(1, maxEdge / Math.max(bmp.width, bmp.height));
const w = Math.max(1, Math.round(bmp.width * s));
const h = Math.max(1, Math.round(bmp.height * s));
const c = document.createElement('canvas');
c.width = w; c.height = h;
c.getContext('2d').drawImage(bmp, 0, 0, w, h);
bmp.close();
const out = await new Promise(res => c.toBlob(res, 'image/jpeg', 0.8));
const b64 = await new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(String(r.result).split(',', 2)[1] || '');
r.onerror = rej;
r.readAsDataURL(out);
});
return { w, h, b64 };
}
async function buildProjectImages() {
if (!currentBomId || !bomImages.length) return [];
const out = [];
for (const img of bomImages.slice(0, 6)) {
try {
const enc = await encodeImage(img.blob, 1400);
if (enc.b64.length > 3 * 1024 * 1024) { trace('image too large after encode, skipping', img.id); continue; }
out.push({ id: img.id, name: img.name || (img.id + '.jpg'), mime: 'image/jpeg', data_base64: enc.b64 });
} catch (e) {
trace('image encode failed', img.id, e && e.message);
}
}
trace('buildProjectImages:', out.length, 'of', bomImages.length, 'photos encoded');
return out;
}
async function applyProjectPatch(patchJson) {
const patch = JSON.parse(patchJson);
if (!currentBomId) { status('Select or create a board first', true); return; }
@ -227,8 +268,10 @@
if (d.type === 'cmms-ai-editor-get-buffer') {
trace('get-buffer received');
let buffer = null;
let images = [];
try { buffer = await buildProjectView(); } catch (e) { trace('buildProjectView threw:', e && e.message); }
replyToChat(evt.source, { type: 'cmms-ai-editor-buffer', buffer: buffer });
try { images = await buildProjectImages(); } catch (e) { trace('buildProjectImages threw:', e && e.message); }
replyToChat(evt.source, { type: 'cmms-ai-editor-buffer', buffer: buffer, images: images });
} else if (d.type === 'cmms-ai-editor-apply' && d.editor === 'pcb' && typeof d.new_source === 'string') {
trace('apply received');
try {