@popmelt.com/core 0.1.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.
@@ -0,0 +1,1745 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
+ var __objRest = (source, exclude) => {
22
+ var target = {};
23
+ for (var prop in source)
24
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
25
+ target[prop] = source[prop];
26
+ if (source != null && __getOwnPropSymbols)
27
+ for (var prop of __getOwnPropSymbols(source)) {
28
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
29
+ target[prop] = source[prop];
30
+ }
31
+ return target;
32
+ };
33
+ var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
34
+
35
+ // src/server/bridge-server.ts
36
+ import { execFileSync } from "child_process";
37
+ import { randomUUID } from "crypto";
38
+ import { mkdir as mkdir2, readdir, stat, unlink, writeFile as writeFile2 } from "fs/promises";
39
+ import { createServer } from "http";
40
+ import { tmpdir } from "os";
41
+ import { join as join2 } from "path";
42
+
43
+ // src/server/claude-spawner.ts
44
+ import { spawn } from "child_process";
45
+ import { createInterface } from "readline";
46
+ function spawnClaude(jobId, options) {
47
+ const {
48
+ prompt,
49
+ projectRoot,
50
+ maxTurns = 10,
51
+ maxBudgetUsd = 1,
52
+ allowedTools = ["Read", "Edit", "Write", "Glob", "Grep", "Bash"],
53
+ claudePath = "claude",
54
+ resumeSessionId,
55
+ model,
56
+ onEvent
57
+ } = options;
58
+ const args = [];
59
+ if (resumeSessionId) {
60
+ args.push("--resume", resumeSessionId, "-p", prompt);
61
+ } else {
62
+ args.push("-p", prompt);
63
+ }
64
+ args.push(
65
+ "--output-format",
66
+ "stream-json",
67
+ "--verbose",
68
+ "--max-turns",
69
+ String(maxTurns),
70
+ "--max-budget-usd",
71
+ String(maxBudgetUsd)
72
+ );
73
+ if (model) {
74
+ args.push("--model", model);
75
+ }
76
+ for (const tool of allowedTools) {
77
+ args.push("--allowedTools", tool);
78
+ }
79
+ const child = spawn(claudePath, args, {
80
+ cwd: projectRoot,
81
+ stdio: ["ignore", "pipe", "pipe"],
82
+ env: __spreadProps(__spreadValues({}, process.env), { ANTHROPIC_API_KEY: void 0 })
83
+ });
84
+ const result = new Promise((resolve) => {
85
+ var _a;
86
+ let capturedSessionId;
87
+ const textChunks = [];
88
+ const fileEdits = [];
89
+ let hadError = false;
90
+ let errorMessage = "";
91
+ const rl = createInterface({ input: child.stdout });
92
+ const seenEventTypes = /* @__PURE__ */ new Set();
93
+ rl.on("line", (line) => {
94
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
95
+ if (!line.trim()) return;
96
+ try {
97
+ const parsed = JSON.parse(line);
98
+ if (parsed.session_id && !capturedSessionId) {
99
+ capturedSessionId = parsed.session_id;
100
+ }
101
+ const topType = (_b = parsed.type) != null ? _b : ((_a2 = parsed.event) == null ? void 0 : _a2.type) ? `event.${parsed.event.type}` : "unknown";
102
+ seenEventTypes.add(topType);
103
+ if (parsed.type === "result" && parsed.result && textChunks.length === 0) {
104
+ const resultText = typeof parsed.result === "string" ? parsed.result : "";
105
+ if (resultText) {
106
+ textChunks.push(resultText);
107
+ onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: resultText }, jobId);
108
+ }
109
+ }
110
+ if (parsed.type === "assistant" && Array.isArray((_c = parsed.message) == null ? void 0 : _c.content)) {
111
+ for (const block of parsed.message.content) {
112
+ if (block.type === "text" && block.text) {
113
+ textChunks.push(block.text);
114
+ onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: block.text }, jobId);
115
+ }
116
+ if (block.type === "tool_use" && block.name) {
117
+ const file = ((_d = block.input) == null ? void 0 : _d.file_path) || ((_e = block.input) == null ? void 0 : _e.path) || void 0;
118
+ onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: block.name }, file ? { file } : {}), jobId);
119
+ if (block.name === "Edit" && ((_f = block.input) == null ? void 0 : _f.file_path)) {
120
+ fileEdits.push({
121
+ tool: "Edit",
122
+ file_path: block.input.file_path,
123
+ old_string: block.input.old_string,
124
+ new_string: block.input.new_string,
125
+ replace_all: block.input.replace_all
126
+ });
127
+ } else if (block.name === "Write" && ((_g = block.input) == null ? void 0 : _g.file_path)) {
128
+ fileEdits.push({
129
+ tool: "Write",
130
+ file_path: block.input.file_path,
131
+ content: block.input.content
132
+ });
133
+ }
134
+ }
135
+ if (block.type === "thinking" && block.thinking) {
136
+ onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: block.thinking }, jobId);
137
+ }
138
+ }
139
+ }
140
+ if (parsed.type === "user" && ((_i = (_h = parsed.tool_use_result) == null ? void 0 : _h.file) == null ? void 0 : _i.filePath)) {
141
+ onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "Read", file: parsed.tool_use_result.file.filePath }, jobId);
142
+ }
143
+ } catch (e) {
144
+ }
145
+ });
146
+ const stderrChunks = [];
147
+ (_a = child.stderr) == null ? void 0 : _a.on("data", (chunk) => {
148
+ stderrChunks.push(chunk.toString());
149
+ });
150
+ child.on("close", (code) => {
151
+ rl.close();
152
+ if (code !== 0 && code !== null) {
153
+ hadError = true;
154
+ errorMessage = stderrChunks.join("") || `Claude process exited with code ${code}`;
155
+ }
156
+ resolve({
157
+ sessionId: capturedSessionId,
158
+ text: textChunks.join(""),
159
+ success: !hadError,
160
+ error: hadError ? errorMessage : void 0,
161
+ fileEdits: fileEdits.length > 0 ? fileEdits : void 0
162
+ });
163
+ });
164
+ child.on("error", (err) => {
165
+ hadError = true;
166
+ errorMessage = err.message;
167
+ resolve({
168
+ sessionId: capturedSessionId,
169
+ text: textChunks.join(""),
170
+ success: false,
171
+ error: errorMessage,
172
+ fileEdits: fileEdits.length > 0 ? fileEdits : void 0
173
+ });
174
+ });
175
+ });
176
+ return { process: child, result };
177
+ }
178
+
179
+ // src/server/codex-spawner.ts
180
+ import { spawn as spawn2 } from "child_process";
181
+ import { createInterface as createInterface2 } from "readline";
182
+ function spawnCodex(jobId, options) {
183
+ const {
184
+ prompt,
185
+ projectRoot,
186
+ screenshotPath,
187
+ resumeSessionId,
188
+ model,
189
+ onEvent
190
+ } = options;
191
+ const args = [];
192
+ if (resumeSessionId) {
193
+ args.push("exec", "resume", resumeSessionId);
194
+ if (model) args.push("-m", model);
195
+ args.push("--json", "--full-auto", prompt);
196
+ if (screenshotPath) {
197
+ args.push("--image", screenshotPath);
198
+ }
199
+ } else {
200
+ args.push("exec", "--json", "--full-auto");
201
+ if (model) args.push("-m", model);
202
+ args.push(prompt);
203
+ if (screenshotPath) {
204
+ args.push("--image", screenshotPath);
205
+ }
206
+ }
207
+ const child = spawn2("codex", args, {
208
+ cwd: projectRoot,
209
+ stdio: ["ignore", "pipe", "pipe"],
210
+ env: __spreadValues({}, process.env)
211
+ });
212
+ const result = new Promise((resolve) => {
213
+ var _a;
214
+ let capturedSessionId;
215
+ const textChunks = [];
216
+ let hadError = false;
217
+ let errorMessage = "";
218
+ const rl = createInterface2({ input: child.stdout });
219
+ const seenEventTypes = /* @__PURE__ */ new Set();
220
+ rl.on("line", (line) => {
221
+ var _a2, _b, _c, _d;
222
+ if (!line.trim()) return;
223
+ try {
224
+ const parsed = JSON.parse(line);
225
+ const eventType = (_a2 = parsed.type) != null ? _a2 : "unknown";
226
+ seenEventTypes.add(eventType);
227
+ if (eventType === "thread.started" && parsed.thread_id && !capturedSessionId) {
228
+ capturedSessionId = parsed.thread_id;
229
+ }
230
+ if (eventType === "item/agentMessage/delta" && ((_b = parsed.delta) == null ? void 0 : _b.text)) {
231
+ textChunks.push(parsed.delta.text);
232
+ onEvent == null ? void 0 : onEvent({ type: "delta", jobId, text: parsed.delta.text }, jobId);
233
+ }
234
+ if (eventType === "item/reasoning/delta" && ((_c = parsed.delta) == null ? void 0 : _c.text)) {
235
+ onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: parsed.delta.text }, jobId);
236
+ }
237
+ if (eventType === "item/started" && parsed.item) {
238
+ const itemType = parsed.item.type;
239
+ if (itemType === "command_execution") {
240
+ onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "Bash" }, jobId);
241
+ } else if (itemType === "file_change") {
242
+ const file = parsed.item.filename || parsed.item.path;
243
+ onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: "Edit" }, file ? { file } : {}), jobId);
244
+ } else if (itemType === "file_read") {
245
+ const file = parsed.item.filename || parsed.item.path;
246
+ onEvent == null ? void 0 : onEvent(__spreadValues({ type: "tool_use", jobId, tool: "Read" }, file ? { file } : {}), jobId);
247
+ } else if (itemType === "web_search") {
248
+ onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: "WebSearch" }, jobId);
249
+ } else if (itemType === "mcp_tool_call") {
250
+ const toolName = parsed.item.tool_name || parsed.item.name || "MCP";
251
+ onEvent == null ? void 0 : onEvent({ type: "tool_use", jobId, tool: toolName }, jobId);
252
+ }
253
+ }
254
+ if (eventType === "item/completed" && parsed.item) {
255
+ if (parsed.item.type === "agent_message") {
256
+ const itemText = parsed.item.text;
257
+ if (typeof itemText === "string" && itemText) {
258
+ textChunks.push(itemText);
259
+ }
260
+ } else if (parsed.item.type === "reasoning") {
261
+ const reasoningText = parsed.item.text;
262
+ if (typeof reasoningText === "string" && reasoningText) {
263
+ onEvent == null ? void 0 : onEvent({ type: "thinking", jobId, text: reasoningText }, jobId);
264
+ }
265
+ }
266
+ }
267
+ if (eventType === "turn.failed") {
268
+ hadError = true;
269
+ errorMessage = ((_d = parsed.error) == null ? void 0 : _d.message) || parsed.message || "Turn failed";
270
+ }
271
+ } catch (e) {
272
+ }
273
+ });
274
+ const stderrChunks = [];
275
+ (_a = child.stderr) == null ? void 0 : _a.on("data", (chunk) => {
276
+ stderrChunks.push(chunk.toString());
277
+ });
278
+ child.on("close", (code) => {
279
+ rl.close();
280
+ if (code !== 0 && code !== null) {
281
+ hadError = true;
282
+ errorMessage = stderrChunks.join("") || `Codex process exited with code ${code}`;
283
+ }
284
+ resolve({
285
+ sessionId: capturedSessionId,
286
+ text: textChunks.join(""),
287
+ success: !hadError,
288
+ error: hadError ? errorMessage : void 0
289
+ });
290
+ });
291
+ child.on("error", (err) => {
292
+ hadError = true;
293
+ errorMessage = err.message;
294
+ resolve({
295
+ sessionId: capturedSessionId,
296
+ text: textChunks.join(""),
297
+ success: false,
298
+ error: errorMessage
299
+ });
300
+ });
301
+ });
302
+ return { process: child, result };
303
+ }
304
+
305
+ // src/server/multipart.ts
306
+ async function parseMultipart(req) {
307
+ const contentType = req.headers["content-type"] || "";
308
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
309
+ if (!boundaryMatch) {
310
+ throw new Error("Missing multipart boundary");
311
+ }
312
+ const boundary = boundaryMatch[1] || boundaryMatch[2];
313
+ const body = await readBody(req);
314
+ const delimiter = Buffer.from(`--${boundary}`);
315
+ const endDelimiter = Buffer.from(`--${boundary}--`);
316
+ let screenshot;
317
+ let feedback;
318
+ let color;
319
+ let provider;
320
+ let model;
321
+ let goal;
322
+ let pageUrl;
323
+ let viewport;
324
+ let planId;
325
+ let manifest;
326
+ let tasks;
327
+ let offset = 0;
328
+ const parts = [];
329
+ while (offset < body.length) {
330
+ const delimStart = body.indexOf(delimiter, offset);
331
+ if (delimStart === -1) break;
332
+ const afterDelim = delimStart + delimiter.length;
333
+ if (body.slice(delimStart, delimStart + endDelimiter.length).equals(endDelimiter)) {
334
+ break;
335
+ }
336
+ let headerStart = afterDelim;
337
+ if (body[headerStart] === 13 && body[headerStart + 1] === 10) {
338
+ headerStart += 2;
339
+ }
340
+ const headerEnd = body.indexOf("\r\n\r\n", headerStart);
341
+ if (headerEnd === -1) break;
342
+ const headers = body.slice(headerStart, headerEnd).toString("utf-8");
343
+ const bodyStart = headerEnd + 4;
344
+ const nextDelim = body.indexOf(delimiter, bodyStart);
345
+ const bodyEnd = nextDelim !== -1 ? nextDelim - 2 : body.length;
346
+ parts.push({
347
+ headers,
348
+ body: body.slice(bodyStart, bodyEnd)
349
+ });
350
+ offset = nextDelim !== -1 ? nextDelim : body.length;
351
+ }
352
+ for (const part of parts) {
353
+ const nameMatch = part.headers.match(/name="([^"]+)"/);
354
+ if (!nameMatch) continue;
355
+ const name = nameMatch[1];
356
+ if (name === "screenshot") {
357
+ screenshot = part.body;
358
+ } else if (name === "feedback") {
359
+ feedback = part.body.toString("utf-8");
360
+ } else if (name === "color") {
361
+ color = part.body.toString("utf-8");
362
+ } else if (name === "provider") {
363
+ provider = part.body.toString("utf-8");
364
+ } else if (name === "model") {
365
+ model = part.body.toString("utf-8");
366
+ } else if (name === "goal") {
367
+ goal = part.body.toString("utf-8");
368
+ } else if (name === "pageUrl") {
369
+ pageUrl = part.body.toString("utf-8");
370
+ } else if (name === "viewport") {
371
+ viewport = part.body.toString("utf-8");
372
+ } else if (name === "planId") {
373
+ planId = part.body.toString("utf-8");
374
+ } else if (name === "manifest") {
375
+ manifest = part.body.toString("utf-8");
376
+ } else if (name === "tasks") {
377
+ tasks = part.body.toString("utf-8");
378
+ }
379
+ }
380
+ if (!screenshot) throw new Error("Missing screenshot field");
381
+ if (!feedback) feedback = "";
382
+ return { screenshot, feedback, color, provider, model, goal, pageUrl, viewport, planId, manifest, tasks };
383
+ }
384
+ function readBody(req) {
385
+ return new Promise((resolve, reject) => {
386
+ const chunks = [];
387
+ req.on("data", (chunk) => chunks.push(chunk));
388
+ req.on("end", () => resolve(Buffer.concat(chunks)));
389
+ req.on("error", reject);
390
+ });
391
+ }
392
+
393
+ // src/server/prompt-builder.ts
394
+ function formatFeedbackContext(feedback) {
395
+ var _a;
396
+ const lines = [];
397
+ if (feedback.annotations.length > 0) {
398
+ lines.push("## Annotations");
399
+ for (const ann of feedback.annotations) {
400
+ const elementsDesc = ann.elements.map((el) => {
401
+ const parts = [el.selector];
402
+ if (el.reactComponent) parts.push(`(${el.reactComponent})`);
403
+ return parts.join(" ");
404
+ }).join(", ");
405
+ const instruction = ann.instruction || "No text";
406
+ lines.push(`- id=${ann.id} [${ann.type}] ${instruction} \u2192 Elements: ${elementsDesc || "none"}`);
407
+ }
408
+ }
409
+ if (feedback.styleModifications.length > 0) {
410
+ lines.push("");
411
+ lines.push("## Style Changes (make permanent in source)");
412
+ lines.push("The developer previewed these CSS changes via inline style overrides. Find the corresponding styles in the source files and update them so the changes persist:");
413
+ for (const mod of feedback.styleModifications) {
414
+ const elementDesc = ((_a = mod.element) == null ? void 0 : _a.reactComponent) ? `(${mod.element.reactComponent})` : "";
415
+ for (const change of mod.changes) {
416
+ lines.push(
417
+ `- ${mod.selector} ${elementDesc}: ${change.property} ${change.original} \u2192 ${change.modified}`
418
+ );
419
+ }
420
+ }
421
+ }
422
+ if (feedback.inspectedElement) {
423
+ const el = feedback.inspectedElement;
424
+ lines.push("");
425
+ lines.push("## Inspected Element");
426
+ lines.push("The developer has this element selected in the inspector:");
427
+ const parts = [el.selector];
428
+ if (el.reactComponent) parts.push(`(${el.reactComponent})`);
429
+ if (el.context) parts.push(`in ${el.context}`);
430
+ if (el.textContent) parts.push(`"${el.textContent.slice(0, 80)}"`);
431
+ lines.push(`- ${parts.join(" ")}`);
432
+ }
433
+ return lines.join("\n");
434
+ }
435
+ function buildPrompt(screenshotPath, feedback, options) {
436
+ const lines = [];
437
+ lines.push("You are reviewing a UI screenshot with developer annotations.");
438
+ lines.push("");
439
+ if ((options == null ? void 0 : options.provider) !== "codex") {
440
+ lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
441
+ lines.push("");
442
+ }
443
+ lines.push(
444
+ `The developer annotated their running app at ${feedback.url} (${feedback.viewport.width}x${feedback.viewport.height}).`
445
+ );
446
+ if ((options == null ? void 0 : options.threadHistory) && options.threadHistory.length > 0) {
447
+ lines.push("");
448
+ lines.push("## Previous Conversation");
449
+ let roundNum = 0;
450
+ for (const msg of options.threadHistory) {
451
+ if (msg.role === "human") {
452
+ roundNum++;
453
+ if (msg.replyToQuestion) {
454
+ lines.push(`### Round ${roundNum} (human) \u2014 reply`);
455
+ lines.push(`"${msg.replyToQuestion}"`);
456
+ } else {
457
+ lines.push(`### Round ${roundNum} (human)`);
458
+ if (msg.feedbackSummary) {
459
+ lines.push(`Annotations: ${msg.feedbackSummary}`);
460
+ }
461
+ if (msg.annotationIds && msg.annotationIds.length > 0) {
462
+ lines.push(`Annotation IDs: ${msg.annotationIds.join(", ")}`);
463
+ }
464
+ }
465
+ } else {
466
+ if (msg.question) {
467
+ lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
468
+ lines.push(`"${msg.question}"`);
469
+ } else {
470
+ lines.push(`### Round ${roundNum} (assistant)`);
471
+ if (msg.responseText) {
472
+ lines.push(`Response: ${msg.responseText}`);
473
+ }
474
+ if (msg.resolutions && msg.resolutions.length > 0) {
475
+ for (const r of msg.resolutions) {
476
+ lines.push(`- ${r.annotationId}: ${r.status} \u2014 ${r.summary}`);
477
+ if (r.filesModified && r.filesModified.length > 0) {
478
+ lines.push(` Files: ${r.filesModified.join(", ")}`);
479
+ }
480
+ }
481
+ }
482
+ if (msg.toolsUsed && msg.toolsUsed.length > 0) {
483
+ lines.push(`Tools used: ${msg.toolsUsed.join(", ")}`);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ lines.push("");
489
+ lines.push("The current round is shown in full below.");
490
+ }
491
+ const feedbackContext = formatFeedbackContext(feedback);
492
+ if (feedbackContext) {
493
+ lines.push("");
494
+ lines.push(feedbackContext);
495
+ }
496
+ lines.push("");
497
+ lines.push(
498
+ "Follow the developer's instructions. If they ask for changes, apply them to the source files \u2014 the dev server has HMR so changes appear immediately. If they ask a question or request analysis, respond in text without modifying code."
499
+ );
500
+ lines.push("");
501
+ lines.push(
502
+ "IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."
503
+ );
504
+ lines.push("");
505
+ lines.push("## Resolution");
506
+ lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
507
+ lines.push("<resolution>");
508
+ lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
509
+ lines.push("</resolution>");
510
+ lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
511
+ lines.push("");
512
+ lines.push("## Questions");
513
+ lines.push("If the annotation text is unclear, ambiguous, gibberish, or you are unsure what the developer wants, output a question:");
514
+ lines.push('<question>What do you mean by "..."?</question>');
515
+ lines.push("Do NOT guess what unclear instructions mean \u2014 ask instead.");
516
+ lines.push("You may output BOTH a <resolution> for clear annotations AND a <question> for unclear ones in the same response.");
517
+ return lines.join("\n");
518
+ }
519
+ function parseQuestion(responseText) {
520
+ var _a;
521
+ const match = responseText.match(/<question>\s*([\s\S]*?)\s*<\/question>/);
522
+ return (_a = match == null ? void 0 : match[1]) != null ? _a : null;
523
+ }
524
+ function buildReplyPrompt(screenshotPath, threadHistory, provider) {
525
+ const lines = [];
526
+ lines.push("You are continuing work on a UI based on the developer's reply to your question.");
527
+ lines.push("");
528
+ if (provider !== "codex") {
529
+ lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
530
+ }
531
+ const firstHuman = threadHistory.find((m) => m.role === "human" && m.feedbackContext);
532
+ if (firstHuman == null ? void 0 : firstHuman.feedbackContext) {
533
+ lines.push("");
534
+ lines.push(firstHuman.feedbackContext);
535
+ }
536
+ if (threadHistory.length > 0) {
537
+ lines.push("");
538
+ lines.push("## Conversation History");
539
+ let roundNum = 0;
540
+ for (const msg of threadHistory) {
541
+ if (msg.role === "human") {
542
+ roundNum++;
543
+ if (msg.replyToQuestion) {
544
+ lines.push(`### Round ${roundNum} (human) \u2014 reply`);
545
+ lines.push(`"${msg.replyToQuestion}"`);
546
+ } else {
547
+ lines.push(`### Round ${roundNum} (human)`);
548
+ if (msg.feedbackSummary) {
549
+ lines.push(`Annotations: ${msg.feedbackSummary}`);
550
+ }
551
+ }
552
+ } else {
553
+ if (msg.question) {
554
+ lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
555
+ lines.push(`"${msg.question}"`);
556
+ } else {
557
+ lines.push(`### Round ${roundNum} (assistant)`);
558
+ if (msg.responseText) {
559
+ lines.push(`Response: ${msg.responseText}`);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ }
565
+ lines.push("");
566
+ lines.push("The developer answered your question. Continue working based on their reply.");
567
+ lines.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately.");
568
+ lines.push("");
569
+ lines.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions.");
570
+ lines.push("");
571
+ lines.push("## Resolution");
572
+ lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
573
+ lines.push("<resolution>");
574
+ lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
575
+ lines.push("</resolution>");
576
+ lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
577
+ lines.push("");
578
+ lines.push("## Questions");
579
+ lines.push("If you still need clarification, output:");
580
+ lines.push("<question>Your question here</question>");
581
+ lines.push("You may output BOTH a <resolution> and a <question> in the same response.");
582
+ return lines.join("\n");
583
+ }
584
+ function isValidResolution(r) {
585
+ return typeof r === "object" && r !== null && typeof r.annotationId === "string" && (r.status === "resolved" || r.status === "needs_review") && typeof r.summary === "string";
586
+ }
587
+ function parseResolutions(responseText) {
588
+ const match = responseText.match(/<resolution>\s*([\s\S]*?)\s*<\/resolution>/);
589
+ if (!match || !match[1]) return [];
590
+ try {
591
+ const parsed = JSON.parse(match[1]);
592
+ if (!Array.isArray(parsed)) return [];
593
+ return parsed.filter(isValidResolution);
594
+ } catch (e) {
595
+ return [];
596
+ }
597
+ }
598
+ function parseAllResolutions(responseText) {
599
+ const results = [];
600
+ const regex = /<resolution>\s*([\s\S]*?)\s*<\/resolution>/g;
601
+ let match;
602
+ while ((match = regex.exec(responseText)) !== null) {
603
+ if (!match[1]) continue;
604
+ try {
605
+ const parsed = JSON.parse(match[1]);
606
+ if (Array.isArray(parsed)) {
607
+ results.push(...parsed.filter(isValidResolution));
608
+ }
609
+ } catch (e) {
610
+ }
611
+ }
612
+ return results;
613
+ }
614
+ function buildPlannerPrompt(screenshotPath, goal, pageUrl, viewport, manifestJson, feedbackContext) {
615
+ const lines = [];
616
+ lines.push("You are a UI design planner. You are looking at a full-page screenshot of a web application.");
617
+ lines.push("");
618
+ lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
619
+ lines.push("");
620
+ lines.push(`Page: ${pageUrl}`);
621
+ lines.push(`Viewport: ${viewport.width}x${viewport.height}`);
622
+ if (manifestJson) {
623
+ lines.push("");
624
+ lines.push("## Page Elements (ground truth)");
625
+ lines.push("Below is a structured inventory of actual DOM elements on this page. Cross-reference");
626
+ lines.push("against this list \u2014 do NOT reference elements that aren't listed here.");
627
+ lines.push("");
628
+ lines.push("<manifest>");
629
+ lines.push(manifestJson);
630
+ lines.push("</manifest>");
631
+ }
632
+ if (feedbackContext) {
633
+ lines.push("");
634
+ lines.push("## Developer Context");
635
+ lines.push("The developer has the following annotations and style changes on their canvas. Factor these into your plan:");
636
+ lines.push(feedbackContext);
637
+ }
638
+ lines.push("");
639
+ lines.push("## Goal");
640
+ lines.push(goal);
641
+ lines.push("");
642
+ lines.push("## Your Task");
643
+ lines.push("Analyze the screenshot and decompose the goal into specific, element-level tasks.");
644
+ lines.push("Each task targets a specific region of the page and gives a clear instruction for a worker agent.");
645
+ lines.push("");
646
+ lines.push("Output your plan as a JSON array inside a <plan> tag. Each task has:");
647
+ lines.push('- `id`: A short unique identifier (e.g., "t1", "t2")');
648
+ lines.push("- `instruction`: Clear, specific instruction for a worker agent (what to change and how)");
649
+ lines.push("- `region`: Bounding box in page coordinates `{x, y, width, height}` \u2014 where (x,y) is top-left corner");
650
+ lines.push("- `priority`: Optional 1-5 (1=highest). Tasks with no dependency can share a priority level.");
651
+ lines.push("");
652
+ lines.push("Example:");
653
+ lines.push("<plan>");
654
+ lines.push("[");
655
+ lines.push(' {"id":"t1","instruction":"Increase heading font-size to 48px and change font-weight to 700","region":{"x":100,"y":50,"width":600,"height":80},"priority":1},');
656
+ lines.push(' {"id":"t2","instruction":"Add a subtle box-shadow to the card container","region":{"x":80,"y":200,"width":640,"height":300},"priority":2}');
657
+ lines.push("]");
658
+ lines.push("</plan>");
659
+ lines.push("");
660
+ lines.push("Guidelines:");
661
+ lines.push("- CRITICAL: Cross-check all element references against the <manifest>. Only reference elements that actually exist. Use the manifest's text content, component names, and bounding rects for precise instructions.");
662
+ lines.push('- Be specific about values (colors, sizes, spacing) rather than vague ("make it look better")');
663
+ lines.push("- Each task should be independently actionable by a worker that can only see its region");
664
+ lines.push("- Regions should tightly bound the relevant UI element(s)");
665
+ lines.push("- Keep tasks atomic \u2014 one change per task, not multiple unrelated changes");
666
+ lines.push("- Order by priority: structural changes first, then visual polish");
667
+ lines.push("- If the goal can be accomplished as a single change, return a plan with just one task. Only decompose when the goal genuinely requires multiple independent changes.");
668
+ lines.push("- If the goal is unclear or you need more context, output a question instead:");
669
+ lines.push("<question>Your question here</question>");
670
+ lines.push("");
671
+ lines.push("Do NOT modify any files. You are a planner only \u2014 output a <plan> or <question>, nothing else.");
672
+ return lines.join("\n");
673
+ }
674
+ function parsePlan(responseText) {
675
+ const match = responseText.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);
676
+ if (!(match == null ? void 0 : match[1])) return null;
677
+ try {
678
+ const parsed = JSON.parse(match[1]);
679
+ if (!Array.isArray(parsed)) return null;
680
+ return parsed.filter(
681
+ (t) => {
682
+ if (typeof t !== "object" || t === null) return false;
683
+ const obj = t;
684
+ if (typeof obj.id !== "string" || typeof obj.instruction !== "string") return false;
685
+ if (typeof obj.region !== "object" || obj.region === null) return false;
686
+ const r = obj.region;
687
+ return typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number";
688
+ }
689
+ );
690
+ } catch (e) {
691
+ return null;
692
+ }
693
+ }
694
+ function buildReviewerPrompt(screenshotPath, goal, completedTasks) {
695
+ const lines = [];
696
+ lines.push("You are reviewing whether a series of UI changes achieved the original design goal.");
697
+ lines.push("");
698
+ lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
699
+ lines.push("");
700
+ lines.push("## Original Goal");
701
+ lines.push(goal);
702
+ lines.push("");
703
+ lines.push("## Completed Tasks");
704
+ for (const task of completedTasks) {
705
+ lines.push(`- [${task.id}] ${task.instruction} \u2192 ${task.summary}`);
706
+ }
707
+ lines.push("");
708
+ lines.push("## Your Task");
709
+ lines.push("Look at the current screenshot and determine if the goal has been achieved.");
710
+ lines.push("Output your verdict inside a <review> tag:");
711
+ lines.push("<review>");
712
+ lines.push('{"verdict":"pass","summary":"The changes look good..."}');
713
+ lines.push("</review>");
714
+ lines.push("");
715
+ lines.push("Or if issues remain:");
716
+ lines.push("<review>");
717
+ lines.push('{"verdict":"fail","summary":"Some issues remain...","issues":["Issue 1","Issue 2"]}');
718
+ lines.push("</review>");
719
+ lines.push("");
720
+ lines.push("Do NOT modify any files. Output only a <review> block.");
721
+ return lines.join("\n");
722
+ }
723
+ function buildPlanExecutorPrompt(screenshotPath, tasks, pageUrl, viewport, provider) {
724
+ const lines = [];
725
+ lines.push("You are implementing a series of UI changes on a web application.");
726
+ lines.push("");
727
+ if (provider !== "codex") {
728
+ lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
729
+ lines.push("");
730
+ }
731
+ lines.push(`Page: ${pageUrl} (${viewport.width}x${viewport.height})`);
732
+ lines.push("");
733
+ lines.push("## Tasks");
734
+ lines.push("Each task targets a specific region of the page. Complete them in order.");
735
+ lines.push("");
736
+ for (const task of tasks) {
737
+ lines.push(`### Task ${task.planTaskId} (annotationId: ${task.annotationId})`);
738
+ lines.push(`Instruction: ${task.instruction}`);
739
+ lines.push(`Region: (${task.region.x}, ${task.region.y}) ${task.region.width}x${task.region.height}`);
740
+ if (task.linkedSelector) {
741
+ lines.push(`Target element: ${task.linkedSelector}`);
742
+ }
743
+ if (task.elements && task.elements.length > 0) {
744
+ const elemDesc = task.elements.map((el) => {
745
+ const parts = [el.selector];
746
+ if (el.reactComponent) parts.push(`(${el.reactComponent})`);
747
+ return parts.join(" ");
748
+ }).join(", ");
749
+ lines.push(`Elements: ${elemDesc}`);
750
+ }
751
+ lines.push("");
752
+ }
753
+ lines.push("## Instructions");
754
+ lines.push("- Apply each change to the source files \u2014 the dev server has HMR so changes appear immediately.");
755
+ lines.push("- IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source.");
756
+ lines.push("- You may use parallel subagents (Task tool) for independent changes, or work serially \u2014 use your judgment.");
757
+ lines.push("");
758
+ lines.push("## Resolution");
759
+ lines.push("CRITICAL: After completing EACH task, immediately output a <resolution> block for that task.");
760
+ lines.push("Do NOT wait until all tasks are done \u2014 output each resolution as soon as that task is finished.");
761
+ lines.push("<resolution>");
762
+ lines.push('[{"annotationId":"<annotationId>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
763
+ lines.push("</resolution>");
764
+ lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
765
+ return lines.join("\n");
766
+ }
767
+ function parseReview(responseText) {
768
+ const match = responseText.match(/<review>\s*([\s\S]*?)\s*<\/review>/);
769
+ if (!(match == null ? void 0 : match[1])) return null;
770
+ try {
771
+ const parsed = JSON.parse(match[1]);
772
+ if (typeof parsed !== "object" || parsed === null) return null;
773
+ if (parsed.verdict !== "pass" && parsed.verdict !== "fail") return null;
774
+ if (typeof parsed.summary !== "string") return null;
775
+ return {
776
+ verdict: parsed.verdict,
777
+ summary: parsed.summary,
778
+ issues: Array.isArray(parsed.issues) ? parsed.issues.filter((i) => typeof i === "string") : void 0
779
+ };
780
+ } catch (e) {
781
+ return null;
782
+ }
783
+ }
784
+
785
+ // src/server/queue.ts
786
+ var JobQueue = class {
787
+ constructor(maxConcurrency = 5) {
788
+ this.queue = [];
789
+ this.activeJobs = /* @__PURE__ */ new Map();
790
+ this.activeProcesses = /* @__PURE__ */ new Map();
791
+ this.listeners = /* @__PURE__ */ new Set();
792
+ this.processor = null;
793
+ this.maxConcurrency = maxConcurrency;
794
+ }
795
+ setProcessor(fn) {
796
+ this.processor = fn;
797
+ }
798
+ /** First active job (backward compat for status endpoint) */
799
+ get active() {
800
+ const first = this.activeJobs.values().next();
801
+ return first.done ? null : first.value;
802
+ }
803
+ get allActive() {
804
+ return Array.from(this.activeJobs.values());
805
+ }
806
+ get activeCount() {
807
+ return this.activeJobs.size;
808
+ }
809
+ get depth() {
810
+ return this.queue.length;
811
+ }
812
+ get isRunning() {
813
+ return this.activeJobs.size > 0;
814
+ }
815
+ setActiveProcess(jobId, proc) {
816
+ if (proc) {
817
+ this.activeProcesses.set(jobId, proc);
818
+ } else {
819
+ this.activeProcesses.delete(jobId);
820
+ }
821
+ }
822
+ enqueue(job) {
823
+ this.queue.push(job);
824
+ this.processNext();
825
+ return this.queue.length + this.activeJobs.size;
826
+ }
827
+ addListener(listener) {
828
+ this.listeners.add(listener);
829
+ return () => this.listeners.delete(listener);
830
+ }
831
+ broadcast(event, jobId) {
832
+ for (const listener of this.listeners) {
833
+ listener(event, jobId);
834
+ }
835
+ }
836
+ cancelJob(jobId) {
837
+ const proc = this.activeProcesses.get(jobId);
838
+ const job = this.activeJobs.get(jobId);
839
+ if (!proc || !job) return false;
840
+ proc.kill("SIGTERM");
841
+ this.activeProcesses.delete(jobId);
842
+ this.activeJobs.delete(jobId);
843
+ job.status = "error";
844
+ job.error = "Cancelled by user";
845
+ this.broadcast(
846
+ { type: "error", jobId: job.id, message: "Cancelled by user" },
847
+ job.id
848
+ );
849
+ this.processNext();
850
+ return true;
851
+ }
852
+ cancelActive() {
853
+ if (this.activeJobs.size === 0) return false;
854
+ const jobIds = Array.from(this.activeJobs.keys());
855
+ for (const jobId of jobIds) {
856
+ this.cancelJob(jobId);
857
+ }
858
+ return true;
859
+ }
860
+ destroy() {
861
+ for (const proc of this.activeProcesses.values()) {
862
+ proc.kill("SIGTERM");
863
+ }
864
+ this.activeProcesses.clear();
865
+ this.activeJobs.clear();
866
+ this.queue = [];
867
+ this.listeners.clear();
868
+ }
869
+ processNext() {
870
+ while (this.activeJobs.size < this.maxConcurrency && this.queue.length > 0 && this.processor) {
871
+ const job = this.queue.shift();
872
+ this.activeJobs.set(job.id, job);
873
+ job.status = "running";
874
+ this.broadcast({ type: "job_started", jobId: job.id, position: 0 }, job.id);
875
+ this.processor(job).catch((err) => {
876
+ job.status = "error";
877
+ job.error = err instanceof Error ? err.message : String(err);
878
+ this.broadcast(
879
+ { type: "error", jobId: job.id, message: job.error },
880
+ job.id
881
+ );
882
+ }).finally(() => {
883
+ this.activeJobs.delete(job.id);
884
+ this.activeProcesses.delete(job.id);
885
+ this.processNext();
886
+ if (this.activeJobs.size === 0 && this.queue.length === 0) {
887
+ this.broadcast({ type: "queue_drained" }, job.id);
888
+ }
889
+ });
890
+ }
891
+ }
892
+ };
893
+
894
+ // src/server/thread-store.ts
895
+ import { mkdir, readFile, writeFile } from "fs/promises";
896
+ import { dirname, join } from "path";
897
+ var EMPTY_STORE = { version: 1, threads: {} };
898
+ var ThreadFileStore = class {
899
+ constructor(projectRoot) {
900
+ this.cache = null;
901
+ this.writeChain = Promise.resolve();
902
+ this.filePath = join(projectRoot, ".popmelt", "threads.json");
903
+ }
904
+ async load() {
905
+ if (this.cache) return this.cache;
906
+ try {
907
+ const raw = await readFile(this.filePath, "utf-8");
908
+ const parsed = JSON.parse(raw);
909
+ if (parsed && parsed.version === 1 && parsed.threads) {
910
+ this.cache = parsed;
911
+ return this.cache;
912
+ }
913
+ } catch (e) {
914
+ }
915
+ this.cache = __spreadProps(__spreadValues({}, EMPTY_STORE), { threads: {} });
916
+ return this.cache;
917
+ }
918
+ async getThread(id) {
919
+ var _a;
920
+ const store = await this.load();
921
+ return (_a = store.threads[id]) != null ? _a : null;
922
+ }
923
+ async findContinuationThread(linkedSelectors) {
924
+ if (linkedSelectors.length === 0) return null;
925
+ const store = await this.load();
926
+ const selectorSet = new Set(linkedSelectors);
927
+ for (const thread of Object.values(store.threads)) {
928
+ const hasOverlap = thread.elementIdentifiers.some((id) => selectorSet.has(id));
929
+ if (hasOverlap) return thread;
930
+ }
931
+ return null;
932
+ }
933
+ async createThread(id, linkedSelectors) {
934
+ const store = await this.load();
935
+ const thread = {
936
+ id,
937
+ createdAt: Date.now(),
938
+ updatedAt: Date.now(),
939
+ elementIdentifiers: linkedSelectors,
940
+ messages: []
941
+ };
942
+ store.threads[id] = thread;
943
+ await this.persist();
944
+ return thread;
945
+ }
946
+ async appendMessage(threadId, message) {
947
+ const store = await this.load();
948
+ const thread = store.threads[threadId];
949
+ if (!thread) return;
950
+ thread.messages.push(message);
951
+ thread.updatedAt = Date.now();
952
+ await this.persist();
953
+ }
954
+ async addElementIdentifiers(threadId, selectors) {
955
+ const store = await this.load();
956
+ const thread = store.threads[threadId];
957
+ if (!thread) return;
958
+ const existing = new Set(thread.elementIdentifiers);
959
+ for (const sel of selectors) {
960
+ if (!existing.has(sel)) {
961
+ thread.elementIdentifiers.push(sel);
962
+ }
963
+ }
964
+ thread.updatedAt = Date.now();
965
+ await this.persist();
966
+ }
967
+ async getThreadHistory(threadId, maxMessages = 6) {
968
+ const thread = await this.getThread(threadId);
969
+ if (!thread || thread.messages.length === 0) return [];
970
+ if (thread.messages.length <= maxMessages) {
971
+ return thread.messages;
972
+ }
973
+ return [
974
+ thread.messages[0],
975
+ ...thread.messages.slice(-(maxMessages - 1))
976
+ ];
977
+ }
978
+ async persist() {
979
+ this.writeChain = this.writeChain.then(async () => {
980
+ if (!this.cache) return;
981
+ try {
982
+ await mkdir(dirname(this.filePath), { recursive: true });
983
+ await writeFile(this.filePath, JSON.stringify(this.cache, null, 2));
984
+ } catch (err) {
985
+ console.error("[ThreadStore] Failed to persist:", err);
986
+ }
987
+ });
988
+ await this.writeChain;
989
+ }
990
+ };
991
+
992
+ // src/server/bridge-server.ts
993
+ var DEFAULT_PORT = 1111;
994
+ var DEFAULT_ALLOWED_TOOLS = ["Read", "Edit", "Write", "Glob", "Grep", "Bash", "WebFetch", "WebSearch", "Bash(curl:*)"];
995
+ var CLEANUP_INTERVAL_MS = 30 * 60 * 1e3;
996
+ var MAX_FILE_AGE_MS = 60 * 60 * 1e3;
997
+ function isLocalhostOrigin(origin) {
998
+ if (!origin) return false;
999
+ try {
1000
+ const url = new URL(origin);
1001
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1";
1002
+ } catch (e) {
1003
+ return false;
1004
+ }
1005
+ }
1006
+ function setCors(req, res) {
1007
+ const origin = req.headers.origin;
1008
+ if (isLocalhostOrigin(origin)) {
1009
+ res.setHeader("Access-Control-Allow-Origin", origin);
1010
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1011
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1012
+ }
1013
+ }
1014
+ function sendJson(res, status, data) {
1015
+ res.writeHead(status, { "Content-Type": "application/json" });
1016
+ res.end(JSON.stringify(data));
1017
+ }
1018
+ function ansiColor(hex, text) {
1019
+ if (!hex) return text;
1020
+ const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
1021
+ if (!m) return text;
1022
+ const [, r, g, b] = m;
1023
+ return `\x1B[38;2;${parseInt(r, 16)};${parseInt(g, 16)};${parseInt(b, 16)}m${text}\x1B[0m`;
1024
+ }
1025
+ function sendSSE(client, event) {
1026
+ try {
1027
+ client.res.write(`event: ${event.type}
1028
+ data: ${JSON.stringify(event)}
1029
+
1030
+ `);
1031
+ } catch (e) {
1032
+ }
1033
+ }
1034
+ async function createPopmelt(options = {}) {
1035
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1036
+ const port = (_a = options.port) != null ? _a : DEFAULT_PORT;
1037
+ const projectRoot = (_b = options.projectRoot) != null ? _b : process.cwd();
1038
+ const tempDir = (_c = options.tempDir) != null ? _c : join2(tmpdir(), "popmelt-bridge");
1039
+ const maxTurns = (_d = options.maxTurns) != null ? _d : 10;
1040
+ const maxBudgetUsd = (_e = options.maxBudgetUsd) != null ? _e : 1;
1041
+ const allowedTools = (_f = options.allowedTools) != null ? _f : DEFAULT_ALLOWED_TOOLS;
1042
+ const claudePath = (_g = options.claudePath) != null ? _g : "claude";
1043
+ const defaultProvider = (_h = options.provider) != null ? _h : "claude";
1044
+ const capabilities = {};
1045
+ for (const cli of ["claude", "codex"]) {
1046
+ try {
1047
+ const cliPath = execFileSync("which", [cli], { encoding: "utf-8" }).trim();
1048
+ capabilities[cli] = { available: true, path: cliPath };
1049
+ } catch (e) {
1050
+ capabilities[cli] = { available: false, path: null };
1051
+ }
1052
+ }
1053
+ await mkdir2(tempDir, { recursive: true });
1054
+ cleanupTempDir(tempDir).catch(() => {
1055
+ });
1056
+ const queue = new JobQueue();
1057
+ const sseClients = /* @__PURE__ */ new Set();
1058
+ const threadStore = new ThreadFileStore(projectRoot);
1059
+ const jobGroups = /* @__PURE__ */ new Map();
1060
+ queue.addListener((event, _jobId) => {
1061
+ for (const client of sseClients) {
1062
+ sendSSE(client, event);
1063
+ }
1064
+ });
1065
+ queue.setProcessor(async (job) => {
1066
+ var _a2, _b2, _c2, _d2, _e2, _f2, _g2;
1067
+ const replyPrompt = job._replyPrompt;
1068
+ const provider = (_a2 = job.provider) != null ? _a2 : defaultProvider;
1069
+ let resumeSessionId;
1070
+ if (job.threadId) {
1071
+ const thread = await threadStore.getThread(job.threadId);
1072
+ if (thread) {
1073
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
1074
+ if (thread.messages[i].sessionId) {
1075
+ resumeSessionId = thread.messages[i].sessionId;
1076
+ break;
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ let prompt;
1082
+ if (resumeSessionId && replyPrompt) {
1083
+ const lastReply = (_b2 = await threadStore.getThread(job.threadId)) == null ? void 0 : _b2.messages.filter((m) => m.role === "human").pop();
1084
+ const replyText = (lastReply == null ? void 0 : lastReply.replyToQuestion) || (lastReply == null ? void 0 : lastReply.feedbackSummary) || "";
1085
+ prompt = replyText + "\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block.";
1086
+ } 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" ? `
1088
+
1089
+ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.screenshotPath}` : "");
1090
+ } else {
1091
+ const threadHistory = !replyPrompt && job.threadId ? await threadStore.getThreadHistory(job.threadId) : void 0;
1092
+ prompt = replyPrompt != null ? replyPrompt : buildPrompt(job.screenshotPath, job.feedback, {
1093
+ threadHistory: threadHistory && threadHistory.length > 0 ? threadHistory : void 0,
1094
+ provider
1095
+ });
1096
+ }
1097
+ const tag = ansiColor(job.color, `[\u22B9 ${port}:${job.id}]`);
1098
+ console.log(`${tag} Reviewing feedback ${job.screenshotPath} (provider: ${provider})${job.threadId ? ` (thread: ${job.threadId})` : ""}${resumeSessionId ? ` (resuming: ${resumeSessionId.slice(0, 8)})` : ""}`);
1099
+ const isPlanExecutor = !!job._isPlanExecutor;
1100
+ let deltaBuffer = "";
1101
+ let lastResolutionCount = 0;
1102
+ const onEvent = (event, jobId) => {
1103
+ queue.broadcast(event, jobId);
1104
+ if (isPlanExecutor && event.type === "delta" && "text" in event) {
1105
+ deltaBuffer += event.text;
1106
+ const resolutions = parseAllResolutions(deltaBuffer);
1107
+ if (resolutions.length > lastResolutionCount) {
1108
+ const newResolutions = resolutions.slice(lastResolutionCount);
1109
+ lastResolutionCount = resolutions.length;
1110
+ queue.broadcast(
1111
+ { type: "task_resolved", jobId, planId: job.planId, resolutions: newResolutions, threadId: job.threadId },
1112
+ jobId
1113
+ );
1114
+ }
1115
+ }
1116
+ };
1117
+ const jobAllowedTools = (_c2 = job._allowedTools) != null ? _c2 : allowedTools;
1118
+ const { process: proc, result } = provider === "codex" ? spawnCodex(job.id, {
1119
+ prompt,
1120
+ projectRoot,
1121
+ screenshotPath: job.screenshotPath,
1122
+ resumeSessionId,
1123
+ model: job.model,
1124
+ onEvent
1125
+ }) : spawnClaude(job.id, {
1126
+ prompt,
1127
+ projectRoot,
1128
+ maxTurns,
1129
+ maxBudgetUsd,
1130
+ allowedTools: jobAllowedTools,
1131
+ claudePath,
1132
+ resumeSessionId,
1133
+ model: job.model,
1134
+ onEvent
1135
+ });
1136
+ queue.setActiveProcess(job.id, proc);
1137
+ const spawnResult = await result;
1138
+ job.result = spawnResult.text;
1139
+ if (spawnResult.success) {
1140
+ console.log(`${tag} Iteration complete`);
1141
+ if (spawnResult.fileEdits && spawnResult.fileEdits.length > 0) {
1142
+ console.log(`${tag} Captured ${spawnResult.fileEdits.length} file edit(s): ${spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path}`).join(", ")}`);
1143
+ }
1144
+ job.status = "done";
1145
+ const question = parseQuestion(spawnResult.text);
1146
+ let resolutions = parseResolutions(spawnResult.text);
1147
+ if (resolutions.length > 0 && job.annotationIds && job.annotationIds.length > 0) {
1148
+ const realIdSet = new Set(job.annotationIds);
1149
+ const allMatch = resolutions.every((r) => realIdSet.has(r.annotationId));
1150
+ if (!allMatch) {
1151
+ resolutions = resolutions.map((r, i) => __spreadProps(__spreadValues({}, r), {
1152
+ annotationId: job.annotationIds[i % job.annotationIds.length]
1153
+ }));
1154
+ }
1155
+ }
1156
+ if (job.threadId) {
1157
+ await threadStore.appendMessage(job.threadId, {
1158
+ role: "assistant",
1159
+ timestamp: Date.now(),
1160
+ jobId: job.id,
1161
+ responseText: spawnResult.text,
1162
+ resolutions: resolutions.length > 0 ? resolutions : void 0,
1163
+ question: question != null ? question : void 0,
1164
+ sessionId: spawnResult.sessionId
1165
+ });
1166
+ }
1167
+ if (job.planId && !job.planTaskId) {
1168
+ const group = jobGroups.get(job.planId);
1169
+ if (group) {
1170
+ const plan = parsePlan(spawnResult.text);
1171
+ if (plan && plan.length > 0) {
1172
+ group.plan = plan;
1173
+ group.status = "awaiting_approval";
1174
+ group.plannerThreadId = job.threadId;
1175
+ console.log(`${tag} Plan ready: ${plan.length} tasks for group ${job.planId}`);
1176
+ queue.broadcast(
1177
+ { type: "plan_ready", jobId: job.id, planId: job.planId, tasks: plan, threadId: job.threadId },
1178
+ job.id
1179
+ );
1180
+ } else if (!question) {
1181
+ group.status = "error";
1182
+ console.error(`${tag} Failed to parse plan from planner response`);
1183
+ }
1184
+ }
1185
+ }
1186
+ if (job.planId && job._isReview) {
1187
+ const group = jobGroups.get(job.planId);
1188
+ if (group) {
1189
+ const review = parseReview(spawnResult.text);
1190
+ if (review) {
1191
+ group.status = review.verdict === "pass" ? "done" : "executing";
1192
+ console.log(`${tag} Review verdict: ${review.verdict} \u2014 ${review.summary}`);
1193
+ queue.broadcast(
1194
+ { type: "plan_review", planId: job.planId, verdict: review.verdict, summary: review.summary, issues: review.issues },
1195
+ job.id
1196
+ );
1197
+ }
1198
+ }
1199
+ }
1200
+ if (question) {
1201
+ console.log(`${tag} \u{1F4AC} Question detected: "${question.slice(0, 120)}" \u2192 broadcasting to ${sseClients.size} SSE clients (threadId=${(_d2 = job.threadId) != null ? _d2 : job.id}, annotationIds=${(_f2 = (_e2 = job.annotationIds) == null ? void 0 : _e2.join(",")) != null ? _f2 : "none"})`);
1202
+ queue.broadcast(
1203
+ { type: "question", jobId: job.id, threadId: (_g2 = job.threadId) != null ? _g2 : job.id, question, annotationIds: job.annotationIds },
1204
+ job.id
1205
+ );
1206
+ }
1207
+ queue.broadcast(
1208
+ { type: "done", jobId: job.id, success: true, resolutions: resolutions.length > 0 ? resolutions : void 0, responseText: spawnResult.text, threadId: job.threadId },
1209
+ job.id
1210
+ );
1211
+ } else {
1212
+ console.error(`${tag} Error: ${spawnResult.error}`);
1213
+ job.status = "error";
1214
+ job.error = spawnResult.error;
1215
+ queue.broadcast(
1216
+ {
1217
+ type: "error",
1218
+ jobId: job.id,
1219
+ message: spawnResult.error || "Unknown error"
1220
+ },
1221
+ job.id
1222
+ );
1223
+ }
1224
+ });
1225
+ const server = createServer(async (req, res) => {
1226
+ setCors(req, res);
1227
+ if (req.method === "OPTIONS") {
1228
+ res.writeHead(204);
1229
+ res.end();
1230
+ return;
1231
+ }
1232
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1233
+ const path = url.pathname;
1234
+ try {
1235
+ if (req.method === "POST" && path === "/send") {
1236
+ await handleSend(req, res);
1237
+ } else if (req.method === "GET" && path === "/events") {
1238
+ handleEvents(req, res);
1239
+ } else if (req.method === "GET" && path === "/status") {
1240
+ handleStatus(res);
1241
+ } else if (req.method === "GET" && path === "/capabilities") {
1242
+ sendJson(res, 200, { providers: capabilities });
1243
+ } else if (req.method === "POST" && path === "/reply") {
1244
+ await handleReply(req, res);
1245
+ } else if (req.method === "POST" && path === "/cancel") {
1246
+ handleCancel(req, res);
1247
+ } else if (req.method === "POST" && path === "/plan") {
1248
+ await handlePlan(req, res);
1249
+ } else if (req.method === "POST" && path === "/plan/approve") {
1250
+ await handlePlanApprove(req, res);
1251
+ } else if (req.method === "POST" && path === "/plan/execute") {
1252
+ await handlePlanExecute(req, res);
1253
+ } else if (req.method === "POST" && path === "/plan/review") {
1254
+ await handlePlanReview(req, res);
1255
+ } else if (req.method === "GET" && path.startsWith("/plan/")) {
1256
+ handleGetPlan(path.slice("/plan/".length), res);
1257
+ } else if (req.method === "GET" && path.startsWith("/thread/")) {
1258
+ const threadId = path.slice("/thread/".length);
1259
+ await handleGetThread(threadId, res);
1260
+ } else {
1261
+ sendJson(res, 404, { error: "Not found" });
1262
+ }
1263
+ } catch (err) {
1264
+ console.error("[Bridge] Request error:", err);
1265
+ sendJson(res, 500, {
1266
+ error: err instanceof Error ? err.message : "Internal error"
1267
+ });
1268
+ }
1269
+ });
1270
+ async function handleSend(req, res) {
1271
+ const { screenshot, feedback: feedbackStr, color, provider: providerStr, model: modelStr } = await parseMultipart(req);
1272
+ let feedback;
1273
+ try {
1274
+ feedback = JSON.parse(feedbackStr);
1275
+ } catch (e) {
1276
+ sendJson(res, 400, { error: "Invalid feedback JSON" });
1277
+ return;
1278
+ }
1279
+ const jobId = randomUUID().slice(0, 8);
1280
+ const screenshotPath = join2(tempDir, `screenshot-${jobId}.png`);
1281
+ await writeFile2(screenshotPath, screenshot);
1282
+ const linkedSelectors = feedback.annotations.map((a) => a.linkedSelector).filter((s) => !!s);
1283
+ let threadId;
1284
+ if (linkedSelectors.length > 0) {
1285
+ const existingThread = await threadStore.findContinuationThread(linkedSelectors);
1286
+ if (existingThread) {
1287
+ threadId = existingThread.id;
1288
+ await threadStore.addElementIdentifiers(threadId, linkedSelectors);
1289
+ } else {
1290
+ const newThread = await threadStore.createThread(jobId, linkedSelectors);
1291
+ threadId = newThread.id;
1292
+ }
1293
+ }
1294
+ const annotationIds = feedback.annotations.map((a) => a.id);
1295
+ const job = {
1296
+ id: jobId,
1297
+ status: "queued",
1298
+ screenshotPath,
1299
+ feedback,
1300
+ createdAt: Date.now(),
1301
+ color,
1302
+ threadId,
1303
+ annotationIds,
1304
+ provider: providerStr === "claude" || providerStr === "codex" ? providerStr : void 0,
1305
+ model: modelStr || void 0
1306
+ };
1307
+ if (threadId) {
1308
+ const feedbackSummary = feedback.annotations.map((a) => a.instruction || `[${a.type}]`).join("; ");
1309
+ const feedbackContext = formatFeedbackContext(feedback);
1310
+ await threadStore.appendMessage(threadId, {
1311
+ role: "human",
1312
+ timestamp: Date.now(),
1313
+ jobId,
1314
+ screenshotPath,
1315
+ annotationIds,
1316
+ feedbackSummary,
1317
+ feedbackContext: feedbackContext || void 0
1318
+ });
1319
+ }
1320
+ const position = queue.enqueue(job);
1321
+ sendJson(res, 200, { jobId, position, threadId });
1322
+ }
1323
+ 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);
1329
+ }
1330
+ } catch (temp) {
1331
+ error = [temp];
1332
+ } finally {
1333
+ try {
1334
+ more && (temp = iter.return) && await temp.call(iter);
1335
+ } finally {
1336
+ if (error)
1337
+ throw error[0];
1338
+ }
1339
+ }
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
+ if (!threadId || !reply) {
1350
+ sendJson(res, 400, { error: "Missing threadId or reply" });
1351
+ return;
1352
+ }
1353
+ const thread = await threadStore.getThread(threadId);
1354
+ if (!thread) {
1355
+ sendJson(res, 404, { error: "Thread not found" });
1356
+ return;
1357
+ }
1358
+ const jobId = randomUUID().slice(0, 8);
1359
+ let screenshotPath = "";
1360
+ {
1361
+ const history2 = await threadStore.getThreadHistory(threadId);
1362
+ for (let i = history2.length - 1; i >= 0; i--) {
1363
+ if (history2[i].screenshotPath) {
1364
+ screenshotPath = history2[i].screenshotPath;
1365
+ break;
1366
+ }
1367
+ }
1368
+ }
1369
+ if (!screenshotPath) {
1370
+ sendJson(res, 400, { error: "No screenshot available" });
1371
+ return;
1372
+ }
1373
+ await threadStore.appendMessage(threadId, {
1374
+ role: "human",
1375
+ timestamp: Date.now(),
1376
+ jobId,
1377
+ replyToQuestion: reply,
1378
+ screenshotPath
1379
+ });
1380
+ const history = await threadStore.getThreadHistory(threadId);
1381
+ const annotationIds = [];
1382
+ for (const msg of history) {
1383
+ if (msg.annotationIds) {
1384
+ for (const id of msg.annotationIds) {
1385
+ if (!annotationIds.includes(id)) annotationIds.push(id);
1386
+ }
1387
+ }
1388
+ }
1389
+ const replyProvider = providerStr === "claude" || providerStr === "codex" ? providerStr : void 0;
1390
+ const prompt = buildReplyPrompt(screenshotPath, history, replyProvider);
1391
+ const job = {
1392
+ id: jobId,
1393
+ status: "queued",
1394
+ screenshotPath,
1395
+ feedback: { timestamp: (/* @__PURE__ */ new Date()).toISOString(), url: "", viewport: { width: 0, height: 0 }, scrollPosition: { x: 0, y: 0 }, annotations: [], styleModifications: [] },
1396
+ createdAt: Date.now(),
1397
+ color,
1398
+ threadId,
1399
+ annotationIds: annotationIds.length > 0 ? annotationIds : void 0,
1400
+ provider: replyProvider,
1401
+ model: modelStr || void 0
1402
+ };
1403
+ job._replyPrompt = prompt;
1404
+ const position = queue.enqueue(job);
1405
+ sendJson(res, 200, { jobId, position, threadId });
1406
+ }
1407
+ function handleEvents(req, res) {
1408
+ res.writeHead(200, {
1409
+ "Content-Type": "text/event-stream",
1410
+ "Cache-Control": "no-cache",
1411
+ Connection: "keep-alive"
1412
+ });
1413
+ res.write(`event: connected
1414
+ data: {"status":"connected"}
1415
+
1416
+ `);
1417
+ const client = { id: randomUUID().slice(0, 8), res };
1418
+ sseClients.add(client);
1419
+ req.on("close", () => {
1420
+ sseClients.delete(client);
1421
+ });
1422
+ }
1423
+ function handleStatus(res) {
1424
+ const allActive = queue.allActive;
1425
+ sendJson(res, 200, {
1426
+ ok: true,
1427
+ activeJob: allActive[0] ? { id: allActive[0].id, status: allActive[0].status } : null,
1428
+ activeJobs: allActive.map((j) => ({ id: j.id, status: j.status })),
1429
+ queueDepth: queue.depth
1430
+ });
1431
+ }
1432
+ function handleCancel(req, res) {
1433
+ const reqUrl = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1434
+ const jobId = reqUrl.searchParams.get("jobId");
1435
+ const cancelled = jobId ? queue.cancelJob(jobId) : queue.cancelActive();
1436
+ sendJson(res, 200, { cancelled });
1437
+ }
1438
+ async function handlePlan(req, res) {
1439
+ const { screenshot, feedback: feedbackStr, goal: goalStr, pageUrl: pageUrlStr, viewport: viewportStr, provider: providerStr, model: modelStr, manifest: manifestStr } = await parseMultipart(req);
1440
+ if (!screenshot || !goalStr) {
1441
+ sendJson(res, 400, { error: "Missing screenshot or goal" });
1442
+ return;
1443
+ }
1444
+ const pageUrl = pageUrlStr || "";
1445
+ let viewport = { width: 1440, height: 900 };
1446
+ try {
1447
+ if (viewportStr) viewport = JSON.parse(viewportStr);
1448
+ } catch (e) {
1449
+ }
1450
+ let feedbackContext;
1451
+ if (feedbackStr) {
1452
+ try {
1453
+ const feedback = JSON.parse(feedbackStr);
1454
+ const ctx = formatFeedbackContext(feedback);
1455
+ if (ctx) feedbackContext = ctx;
1456
+ } catch (e) {
1457
+ }
1458
+ }
1459
+ const planId = randomUUID().slice(0, 8);
1460
+ const jobId = randomUUID().slice(0, 8);
1461
+ const screenshotPath = join2(tempDir, `screenshot-plan-${planId}.png`);
1462
+ await writeFile2(screenshotPath, screenshot);
1463
+ const thread = await threadStore.createThread(jobId, []);
1464
+ const threadId = thread.id;
1465
+ const group = {
1466
+ id: planId,
1467
+ goal: goalStr,
1468
+ status: "planning",
1469
+ plannerJobId: jobId,
1470
+ plannerThreadId: threadId,
1471
+ workerJobIds: [],
1472
+ screenshotPath,
1473
+ pageUrl,
1474
+ viewport,
1475
+ createdAt: Date.now()
1476
+ };
1477
+ jobGroups.set(planId, group);
1478
+ const prompt = buildPlannerPrompt(screenshotPath, goalStr, pageUrl, viewport, manifestStr, feedbackContext);
1479
+ await threadStore.appendMessage(threadId, {
1480
+ role: "human",
1481
+ timestamp: Date.now(),
1482
+ jobId,
1483
+ screenshotPath,
1484
+ feedbackSummary: `Plan: ${goalStr}`,
1485
+ feedbackContext: `Goal: ${goalStr}
1486
+ Page: ${pageUrl}`
1487
+ });
1488
+ const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1489
+ const job = {
1490
+ id: jobId,
1491
+ status: "queued",
1492
+ screenshotPath,
1493
+ feedback: {
1494
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1495
+ url: pageUrl,
1496
+ viewport,
1497
+ scrollPosition: { x: 0, y: 0 },
1498
+ annotations: [],
1499
+ styleModifications: []
1500
+ },
1501
+ createdAt: Date.now(),
1502
+ threadId,
1503
+ provider,
1504
+ model: modelStr || void 0,
1505
+ planId
1506
+ };
1507
+ job._replyPrompt = prompt;
1508
+ job._allowedTools = ["Read"];
1509
+ const position = queue.enqueue(job);
1510
+ sendJson(res, 200, { planId, jobId, position, threadId });
1511
+ }
1512
+ async function handlePlanApprove(req, res) {
1513
+ const chunks = [];
1514
+ try {
1515
+ for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1516
+ const chunk = temp.value;
1517
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1518
+ }
1519
+ } catch (temp) {
1520
+ error = [temp];
1521
+ } finally {
1522
+ try {
1523
+ more && (temp = iter.return) && await temp.call(iter);
1524
+ } finally {
1525
+ if (error)
1526
+ throw error[0];
1527
+ }
1528
+ }
1529
+ const body = Buffer.concat(chunks).toString("utf-8");
1530
+ let parsed;
1531
+ try {
1532
+ parsed = JSON.parse(body);
1533
+ } catch (e) {
1534
+ sendJson(res, 400, { error: "Invalid JSON" });
1535
+ return;
1536
+ }
1537
+ const { planId, approvedTaskIds } = parsed;
1538
+ if (!planId) {
1539
+ sendJson(res, 400, { error: "Missing planId" });
1540
+ return;
1541
+ }
1542
+ const group = jobGroups.get(planId);
1543
+ if (!group) {
1544
+ sendJson(res, 404, { error: "Plan not found" });
1545
+ return;
1546
+ }
1547
+ if (!group.plan) {
1548
+ sendJson(res, 400, { error: "Plan has no tasks" });
1549
+ return;
1550
+ }
1551
+ const tasks = approvedTaskIds ? group.plan.filter((t) => approvedTaskIds.includes(t.id)) : group.plan;
1552
+ group.status = "executing";
1553
+ sendJson(res, 200, { planId, tasks, status: "executing" });
1554
+ }
1555
+ async function handlePlanExecute(req, res) {
1556
+ const { screenshot, planId: planIdStr, tasks: tasksStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1557
+ if (!planIdStr || !tasksStr || !screenshot) {
1558
+ sendJson(res, 400, { error: "Missing planId, tasks, or screenshot" });
1559
+ return;
1560
+ }
1561
+ const group = jobGroups.get(planIdStr);
1562
+ if (!group) {
1563
+ sendJson(res, 404, { error: "Plan not found" });
1564
+ return;
1565
+ }
1566
+ if (group.status !== "executing") {
1567
+ sendJson(res, 400, { error: `Plan status is ${group.status}, expected executing` });
1568
+ return;
1569
+ }
1570
+ let tasks;
1571
+ try {
1572
+ tasks = JSON.parse(tasksStr);
1573
+ } catch (e) {
1574
+ sendJson(res, 400, { error: "Invalid tasks JSON" });
1575
+ return;
1576
+ }
1577
+ const jobId = randomUUID().slice(0, 8);
1578
+ const screenshotPath = join2(tempDir, `screenshot-exec-${planIdStr}.png`);
1579
+ await writeFile2(screenshotPath, screenshot);
1580
+ const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1581
+ const prompt = buildPlanExecutorPrompt(screenshotPath, tasks, group.pageUrl, group.viewport, provider);
1582
+ const annotationIds = tasks.map((t) => t.annotationId);
1583
+ const job = {
1584
+ id: jobId,
1585
+ status: "queued",
1586
+ screenshotPath,
1587
+ feedback: {
1588
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1589
+ url: group.pageUrl,
1590
+ viewport: group.viewport,
1591
+ scrollPosition: { x: 0, y: 0 },
1592
+ annotations: [],
1593
+ styleModifications: []
1594
+ },
1595
+ createdAt: Date.now(),
1596
+ provider,
1597
+ model: modelStr || void 0,
1598
+ planId: planIdStr,
1599
+ annotationIds
1600
+ };
1601
+ job._replyPrompt = prompt;
1602
+ job._isPlanExecutor = true;
1603
+ group.executorJobId = jobId;
1604
+ const position = queue.enqueue(job);
1605
+ sendJson(res, 200, { jobId, planId: planIdStr, position });
1606
+ }
1607
+ async function handlePlanReview(req, res) {
1608
+ const { screenshot, planId: planIdStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1609
+ if (!planIdStr) {
1610
+ sendJson(res, 400, { error: "Missing planId" });
1611
+ return;
1612
+ }
1613
+ const group = jobGroups.get(planIdStr);
1614
+ if (!group) {
1615
+ sendJson(res, 404, { error: "Plan not found" });
1616
+ return;
1617
+ }
1618
+ group.status = "reviewing";
1619
+ const jobId = randomUUID().slice(0, 8);
1620
+ let screenshotPath = group.screenshotPath;
1621
+ if (screenshot) {
1622
+ screenshotPath = join2(tempDir, `screenshot-review-${planIdStr}.png`);
1623
+ await writeFile2(screenshotPath, screenshot);
1624
+ }
1625
+ const completedTasks = (group.plan || []).map((t) => ({
1626
+ id: t.id,
1627
+ instruction: t.instruction,
1628
+ summary: "completed"
1629
+ // Workers will have set resolution summaries
1630
+ }));
1631
+ const prompt = buildReviewerPrompt(screenshotPath, group.goal, completedTasks);
1632
+ const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1633
+ const job = {
1634
+ id: jobId,
1635
+ status: "queued",
1636
+ screenshotPath,
1637
+ feedback: {
1638
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1639
+ url: group.pageUrl,
1640
+ viewport: group.viewport,
1641
+ scrollPosition: { x: 0, y: 0 },
1642
+ annotations: [],
1643
+ styleModifications: []
1644
+ },
1645
+ createdAt: Date.now(),
1646
+ // Don't set threadId — avoids resuming planner session, which would discard the review prompt
1647
+ provider,
1648
+ model: modelStr || void 0,
1649
+ planId: planIdStr
1650
+ };
1651
+ job._replyPrompt = prompt;
1652
+ job._isReview = true;
1653
+ job._allowedTools = ["Read"];
1654
+ const position = queue.enqueue(job);
1655
+ sendJson(res, 200, { jobId, planId: planIdStr, position });
1656
+ }
1657
+ function handleGetPlan(planId, res) {
1658
+ const group = jobGroups.get(planId);
1659
+ if (!group) {
1660
+ sendJson(res, 404, { error: "Plan not found" });
1661
+ return;
1662
+ }
1663
+ sendJson(res, 200, group);
1664
+ }
1665
+ async function handleGetThread(threadId, res) {
1666
+ const thread = await threadStore.getThread(threadId);
1667
+ if (!thread) {
1668
+ sendJson(res, 404, { error: "Thread not found" });
1669
+ return;
1670
+ }
1671
+ const messages = thread.messages.map((_a2) => {
1672
+ var _b2 = _a2, { screenshotPath } = _b2, rest = __objRest(_b2, ["screenshotPath"]);
1673
+ return rest;
1674
+ });
1675
+ sendJson(res, 200, { id: thread.id, createdAt: thread.createdAt, messages });
1676
+ }
1677
+ const cleanupTimer = setInterval(() => {
1678
+ cleanupTempDir(tempDir).catch(() => {
1679
+ });
1680
+ }, CLEANUP_INTERVAL_MS);
1681
+ return new Promise((resolve, reject) => {
1682
+ server.on("error", (err) => {
1683
+ if (err.code === "EADDRINUSE") {
1684
+ console.log(`[\u22B9 already watching :${port}]`);
1685
+ resolve({
1686
+ port,
1687
+ close: async () => {
1688
+ }
1689
+ });
1690
+ return;
1691
+ }
1692
+ reject(err);
1693
+ });
1694
+ server.listen(port, "127.0.0.1", () => {
1695
+ console.log(`[\u22B9 is watching :${port}]`);
1696
+ resolve({
1697
+ port,
1698
+ close: async () => {
1699
+ clearInterval(cleanupTimer);
1700
+ queue.destroy();
1701
+ for (const client of sseClients) {
1702
+ try {
1703
+ client.res.end();
1704
+ } catch (e) {
1705
+ }
1706
+ }
1707
+ sseClients.clear();
1708
+ return new Promise((res) => {
1709
+ server.close(() => res());
1710
+ });
1711
+ }
1712
+ });
1713
+ });
1714
+ });
1715
+ }
1716
+ async function cleanupTempDir(tempDir) {
1717
+ try {
1718
+ const files = await readdir(tempDir);
1719
+ const now = Date.now();
1720
+ for (const file of files) {
1721
+ const filePath = join2(tempDir, file);
1722
+ try {
1723
+ const stats = await stat(filePath);
1724
+ if (now - stats.mtimeMs > MAX_FILE_AGE_MS) {
1725
+ await unlink(filePath);
1726
+ }
1727
+ } catch (e) {
1728
+ }
1729
+ }
1730
+ } catch (e) {
1731
+ }
1732
+ }
1733
+
1734
+ // src/server/index.ts
1735
+ async function startPopmelt(options) {
1736
+ if (process.env.NODE_ENV === "production" && !(options == null ? void 0 : options.force)) {
1737
+ throw new Error(
1738
+ "[Bridge] Refusing to start in production. Pass { force: true } to override."
1739
+ );
1740
+ }
1741
+ return createPopmelt(options);
1742
+ }
1743
+ export {
1744
+ startPopmelt
1745
+ };