@popmelt.com/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -324,6 +324,7 @@ async function parseMultipart(req) {
324
324
  let planId;
325
325
  let manifest;
326
326
  let tasks;
327
+ const pastedImages = [];
327
328
  let offset = 0;
328
329
  const parts = [];
329
330
  while (offset < body.length) {
@@ -375,11 +376,18 @@ async function parseMultipart(req) {
375
376
  manifest = part.body.toString("utf-8");
376
377
  } else if (name === "tasks") {
377
378
  tasks = part.body.toString("utf-8");
379
+ } else if (name.startsWith("image-")) {
380
+ const segments = name.split("-");
381
+ const idx = parseInt(segments[segments.length - 1], 10);
382
+ const annotationId = segments.slice(1, -1).join("-");
383
+ if (annotationId && !isNaN(idx)) {
384
+ pastedImages.push({ annotationId, index: idx, data: part.body });
385
+ }
378
386
  }
379
387
  }
380
388
  if (!screenshot) throw new Error("Missing screenshot field");
381
389
  if (!feedback) feedback = "";
382
- return { screenshot, feedback, color, provider, model, goal, pageUrl, viewport, planId, manifest, tasks };
390
+ return { screenshot, feedback, color, provider, model, goal, pageUrl, viewport, planId, manifest, tasks, pastedImages };
383
391
  }
384
392
  function readBody(req) {
385
393
  return new Promise((resolve, reject) => {
@@ -391,7 +399,7 @@ function readBody(req) {
391
399
  }
392
400
 
393
401
  // src/server/prompt-builder.ts
394
- function formatFeedbackContext(feedback) {
402
+ function formatFeedbackContext(feedback, imagePaths) {
395
403
  var _a;
396
404
  const lines = [];
397
405
  if (feedback.annotations.length > 0) {
@@ -404,6 +412,12 @@ function formatFeedbackContext(feedback) {
404
412
  }).join(", ");
405
413
  const instruction = ann.instruction || "No text";
406
414
  lines.push(`- id=${ann.id} [${ann.type}] ${instruction} \u2192 Elements: ${elementsDesc || "none"}`);
415
+ const annImages = imagePaths == null ? void 0 : imagePaths[ann.id];
416
+ if (annImages && annImages.length > 0) {
417
+ for (const imgPath of annImages) {
418
+ lines.push(` Attached image: use the Read tool to view ${imgPath}`);
419
+ }
420
+ }
407
421
  }
408
422
  }
409
423
  if (feedback.styleModifications.length > 0) {
@@ -488,7 +502,7 @@ function buildPrompt(screenshotPath, feedback, options) {
488
502
  lines.push("");
489
503
  lines.push("The current round is shown in full below.");
490
504
  }
491
- const feedbackContext = formatFeedbackContext(feedback);
505
+ const feedbackContext = formatFeedbackContext(feedback, options == null ? void 0 : options.imagePaths);
492
506
  if (feedbackContext) {
493
507
  lines.push("");
494
508
  lines.push(feedbackContext);
@@ -521,7 +535,7 @@ function parseQuestion(responseText) {
521
535
  const match = responseText.match(/<question>\s*([\s\S]*?)\s*<\/question>/);
522
536
  return (_a = match == null ? void 0 : match[1]) != null ? _a : null;
523
537
  }
524
- function buildReplyPrompt(screenshotPath, threadHistory, provider) {
538
+ function buildReplyPrompt(screenshotPath, threadHistory, provider, imagePaths) {
525
539
  const lines = [];
526
540
  lines.push("You are continuing work on a UI based on the developer's reply to your question.");
527
541
  lines.push("");
@@ -567,6 +581,14 @@ function buildReplyPrompt(screenshotPath, threadHistory, provider) {
567
581
  lines.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately.");
568
582
  lines.push("");
569
583
  lines.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions.");
584
+ if (imagePaths && imagePaths.length > 0) {
585
+ lines.push("");
586
+ lines.push("## Attached Images");
587
+ lines.push("The developer attached reference images with their reply:");
588
+ for (const imgPath of imagePaths) {
589
+ lines.push(`Attached image: use the Read tool to view the image at: ${imgPath}`);
590
+ }
591
+ }
570
592
  lines.push("");
571
593
  lines.push("## Resolution");
572
594
  lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
@@ -1084,14 +1106,15 @@ async function createPopmelt(options = {}) {
1084
1106
  const replyText = (lastReply == null ? void 0 : lastReply.replyToQuestion) || (lastReply == null ? void 0 : lastReply.feedbackSummary) || "";
1085
1107
  prompt = replyText + "\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block.";
1086
1108
  } else if (resumeSessionId) {
1087
- prompt = formatFeedbackContext(job.feedback) + "\n\nFollow the developer's instructions. If they ask for changes, apply them to the source files.\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block." + (provider !== "codex" ? `
1109
+ prompt = formatFeedbackContext(job.feedback, job.imagePaths) + "\n\nFollow the developer's instructions. If they ask for changes, apply them to the source files.\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block." + (provider !== "codex" ? `
1088
1110
 
1089
1111
  IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.screenshotPath}` : "");
1090
1112
  } else {
1091
1113
  const threadHistory = !replyPrompt && job.threadId ? await threadStore.getThreadHistory(job.threadId) : void 0;
1092
1114
  prompt = replyPrompt != null ? replyPrompt : buildPrompt(job.screenshotPath, job.feedback, {
1093
1115
  threadHistory: threadHistory && threadHistory.length > 0 ? threadHistory : void 0,
1094
- provider
1116
+ provider,
1117
+ imagePaths: job.imagePaths
1095
1118
  });
1096
1119
  }
1097
1120
  const tag = ansiColor(job.color, `[\u22B9 ${port}:${job.id}]`);
@@ -1153,6 +1176,7 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1153
1176
  }));
1154
1177
  }
1155
1178
  }
1179
+ const toolsUsed = spawnResult.fileEdits && spawnResult.fileEdits.length > 0 ? spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path.split("/").pop()}`) : void 0;
1156
1180
  if (job.threadId) {
1157
1181
  await threadStore.appendMessage(job.threadId, {
1158
1182
  role: "assistant",
@@ -1161,7 +1185,8 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1161
1185
  responseText: spawnResult.text,
1162
1186
  resolutions: resolutions.length > 0 ? resolutions : void 0,
1163
1187
  question: question != null ? question : void 0,
1164
- sessionId: spawnResult.sessionId
1188
+ sessionId: spawnResult.sessionId,
1189
+ toolsUsed
1165
1190
  });
1166
1191
  }
1167
1192
  if (job.planId && !job.planTaskId) {
@@ -1268,7 +1293,7 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1268
1293
  }
1269
1294
  });
1270
1295
  async function handleSend(req, res) {
1271
- const { screenshot, feedback: feedbackStr, color, provider: providerStr, model: modelStr } = await parseMultipart(req);
1296
+ const { screenshot, feedback: feedbackStr, color, provider: providerStr, model: modelStr, pastedImages } = await parseMultipart(req);
1272
1297
  let feedback;
1273
1298
  try {
1274
1299
  feedback = JSON.parse(feedbackStr);
@@ -1279,6 +1304,15 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1279
1304
  const jobId = randomUUID().slice(0, 8);
1280
1305
  const screenshotPath = join2(tempDir, `screenshot-${jobId}.png`);
1281
1306
  await writeFile2(screenshotPath, screenshot);
1307
+ const imagePaths = {};
1308
+ if (pastedImages.length > 0) {
1309
+ for (const img of pastedImages) {
1310
+ const imgPath = join2(tempDir, `pasted-${jobId}-${img.annotationId}-${img.index}.png`);
1311
+ await writeFile2(imgPath, img.data);
1312
+ if (!imagePaths[img.annotationId]) imagePaths[img.annotationId] = [];
1313
+ imagePaths[img.annotationId].push(imgPath);
1314
+ }
1315
+ }
1282
1316
  const linkedSelectors = feedback.annotations.map((a) => a.linkedSelector).filter((s) => !!s);
1283
1317
  let threadId;
1284
1318
  if (linkedSelectors.length > 0) {
@@ -1292,7 +1326,7 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1292
1326
  }
1293
1327
  }
1294
1328
  const annotationIds = feedback.annotations.map((a) => a.id);
1295
- const job = {
1329
+ const job = __spreadValues({
1296
1330
  id: jobId,
1297
1331
  status: "queued",
1298
1332
  screenshotPath,
@@ -1303,10 +1337,10 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1303
1337
  annotationIds,
1304
1338
  provider: providerStr === "claude" || providerStr === "codex" ? providerStr : void 0,
1305
1339
  model: modelStr || void 0
1306
- };
1340
+ }, Object.keys(imagePaths).length > 0 ? { imagePaths } : {});
1307
1341
  if (threadId) {
1308
1342
  const feedbackSummary = feedback.annotations.map((a) => a.instruction || `[${a.type}]`).join("; ");
1309
- const feedbackContext = formatFeedbackContext(feedback);
1343
+ const feedbackContext = formatFeedbackContext(feedback, Object.keys(imagePaths).length > 0 ? imagePaths : void 0);
1310
1344
  await threadStore.appendMessage(threadId, {
1311
1345
  role: "human",
1312
1346
  timestamp: Date.now(),
@@ -1321,31 +1355,55 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1321
1355
  sendJson(res, 200, { jobId, position, threadId });
1322
1356
  }
1323
1357
  async function handleReply(req, res) {
1324
- const chunks = [];
1325
- try {
1326
- for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1327
- const chunk = temp.value;
1328
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1358
+ const contentType = req.headers["content-type"] || "";
1359
+ let threadId;
1360
+ let reply;
1361
+ let color;
1362
+ let providerStr;
1363
+ let modelStr;
1364
+ let replyImageBuffers = [];
1365
+ if (contentType.includes("multipart/form-data")) {
1366
+ const parsed = await parseMultipart(req);
1367
+ const meta = parsed.feedback ? JSON.parse(parsed.feedback) : {};
1368
+ threadId = meta.threadId;
1369
+ reply = meta.reply;
1370
+ color = meta.color;
1371
+ providerStr = meta.provider;
1372
+ modelStr = meta.model;
1373
+ for (const img of parsed.pastedImages) {
1374
+ replyImageBuffers.push(img.data);
1329
1375
  }
1330
- } catch (temp) {
1331
- error = [temp];
1332
- } finally {
1376
+ } else {
1377
+ const chunks = [];
1333
1378
  try {
1334
- more && (temp = iter.return) && await temp.call(iter);
1379
+ for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1380
+ const chunk = temp.value;
1381
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1382
+ }
1383
+ } catch (temp) {
1384
+ error = [temp];
1335
1385
  } finally {
1336
- if (error)
1337
- throw error[0];
1386
+ try {
1387
+ more && (temp = iter.return) && await temp.call(iter);
1388
+ } finally {
1389
+ if (error)
1390
+ throw error[0];
1391
+ }
1338
1392
  }
1393
+ const body = Buffer.concat(chunks).toString("utf-8");
1394
+ let parsed;
1395
+ try {
1396
+ parsed = JSON.parse(body);
1397
+ } catch (e) {
1398
+ sendJson(res, 400, { error: "Invalid JSON" });
1399
+ return;
1400
+ }
1401
+ threadId = parsed.threadId;
1402
+ reply = parsed.reply;
1403
+ color = parsed.color;
1404
+ providerStr = parsed.provider;
1405
+ modelStr = parsed.model;
1339
1406
  }
1340
- const body = Buffer.concat(chunks).toString("utf-8");
1341
- let parsed;
1342
- try {
1343
- parsed = JSON.parse(body);
1344
- } catch (e) {
1345
- sendJson(res, 400, { error: "Invalid JSON" });
1346
- return;
1347
- }
1348
- const { threadId, reply, color, provider: providerStr, model: modelStr } = parsed;
1349
1407
  if (!threadId || !reply) {
1350
1408
  sendJson(res, 400, { error: "Missing threadId or reply" });
1351
1409
  return;
@@ -1356,6 +1414,12 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1356
1414
  return;
1357
1415
  }
1358
1416
  const jobId = randomUUID().slice(0, 8);
1417
+ const replyImagePaths = [];
1418
+ for (let i = 0; i < replyImageBuffers.length; i++) {
1419
+ const imgPath = join2(tempDir, `reply-${jobId}-${i}.png`);
1420
+ await writeFile2(imgPath, replyImageBuffers[i]);
1421
+ replyImagePaths.push(imgPath);
1422
+ }
1359
1423
  let screenshotPath = "";
1360
1424
  {
1361
1425
  const history2 = await threadStore.getThreadHistory(threadId);
@@ -1387,7 +1451,12 @@ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.scr
1387
1451
  }
1388
1452
  }
1389
1453
  const replyProvider = providerStr === "claude" || providerStr === "codex" ? providerStr : void 0;
1390
- const prompt = buildReplyPrompt(screenshotPath, history, replyProvider);
1454
+ const prompt = buildReplyPrompt(
1455
+ screenshotPath,
1456
+ history,
1457
+ replyProvider,
1458
+ replyImagePaths.length > 0 ? replyImagePaths : void 0
1459
+ );
1391
1460
  const job = {
1392
1461
  id: jobId,
1393
1462
  status: "queued",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popmelt.com/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The design collaboration layer for AI coding agents",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "author": "Popmelt <hello@popmelt.com> (https://popmelt.com)",
@@ -30,7 +30,6 @@
30
30
  "format": "prettier --check \"**/*.{ts,tsx}\"",
31
31
  "lint": "eslint .",
32
32
  "prepublishOnly": "pnpm run build",
33
- "test": "vitest run",
34
33
  "typecheck": "tsc --noEmit"
35
34
  },
36
35
  "exports": {
@@ -73,7 +72,6 @@
73
72
  "react": "19.1.0",
74
73
  "react-dom": "19.1.0",
75
74
  "tsup": "^8.5.1",
76
- "typescript": "^5.8.3",
77
- "vitest": "^4.0.18"
75
+ "typescript": "^5.8.3"
78
76
  }
79
77
  }