@popmelt.com/core 0.1.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/README.md +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1287 -811
- package/dist/server.mjs +101 -32
- package/package.json +2 -4
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
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
}
|
|
1331
|
-
|
|
1332
|
-
} finally {
|
|
1376
|
+
} else {
|
|
1377
|
+
const chunks = [];
|
|
1333
1378
|
try {
|
|
1334
|
-
more
|
|
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
|
-
|
|
1337
|
-
|
|
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(
|
|
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.
|
|
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
|
}
|