@popmelt.com/core 0.3.0 → 0.3.1

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.
Files changed (3) hide show
  1. package/dist/index.mjs +16 -12967
  2. package/dist/server.mjs +17 -1807
  3. package/package.json +1 -1
package/dist/server.mjs CHANGED
@@ -1,1814 +1,24 @@
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);
1
+ var qe=Object.defineProperty,He=Object.defineProperties;var Ge=Object.getOwnPropertyDescriptors;var me=Object.getOwnPropertySymbols;var Pe=Object.prototype.hasOwnProperty,xe=Object.prototype.propertyIsEnumerable;var Ie=(n,e)=>(e=Symbol[n])?e:Symbol.for("Symbol."+n);var Se=(n,e,s)=>e in n?qe(n,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):n[e]=s,ee=(n,e)=>{for(var s in e||(e={}))Pe.call(e,s)&&Se(n,s,e[s]);if(me)for(var s of me(e))xe.call(e,s)&&Se(n,s,e[s]);return n},ie=(n,e)=>He(n,Ge(e));var be=(n,e)=>{var s={};for(var t in n)Pe.call(n,t)&&e.indexOf(t)<0&&(s[t]=n[t]);if(n!=null&&me)for(var t of me(n))e.indexOf(t)<0&&xe.call(n,t)&&(s[t]=n[t]);return s};var ve=(n,e,s)=>(e=n[Ie("asyncIterator")])?e.call(n):(n=n[Ie("iterator")](),e={},s=(t,r)=>(r=n[t])&&(e[t]=l=>new Promise((i,u,m)=>(l=r.call(n,l),m=l.done,Promise.resolve(l.value).then(_=>i({value:_,done:m}),u)))),s("next"),s("return"),e);import{execFileSync as tt}from"child_process";import{randomUUID as re}from"crypto";import{mkdir as st,readdir as nt,stat as ot,unlink as rt,writeFile as le}from"fs/promises";import{createServer as it}from"http";import{tmpdir as at}from"os";import{join as oe}from"path";import{spawn as Le}from"child_process";import{createInterface as Ye}from"readline";function Te(n,e){let{prompt:s,projectRoot:t,maxTurns:r=10,maxBudgetUsd:l=1,allowedTools:i=["Read","Edit","Write","Glob","Grep","Bash"],claudePath:u="claude",resumeSessionId:m,model:_,onEvent:h}=e,U=[];m?U.push("--resume",m,"-p",s):U.push("-p",s),U.push("--output-format","stream-json","--verbose","--max-turns",String(r),"--max-budget-usd",String(l)),_&&U.push("--model",_);for(let L of i)U.push("--allowedTools",L);let x=Le(u,U,{cwd:t,stdio:["ignore","pipe","pipe"],env:ie(ee({},process.env),{ANTHROPIC_API_KEY:void 0})}),G=new Promise(L=>{var T;let Y,z=[],V=[],X=!1,K="",q=Ye({input:x.stdout}),R=new Set;q.on("line",A=>{var y,H,C,Q,ue,de,ce,pe,he;if(A.trim())try{let B=JSON.parse(A);B.session_id&&!Y&&(Y=B.session_id);let o=(H=B.type)!=null?H:(y=B.event)!=null&&y.type?`event.${B.event.type}`:"unknown";if(R.add(o),B.type==="result"&&B.result&&z.length===0){let a=typeof B.result=="string"?B.result:"";a&&(z.push(a),h==null||h({type:"delta",jobId:n,text:a},n))}if(B.type==="assistant"&&Array.isArray((C=B.message)==null?void 0:C.content))for(let a of B.message.content){if(a.type==="text"&&a.text&&(z.push(a.text),h==null||h({type:"delta",jobId:n,text:a.text},n)),a.type==="tool_use"&&a.name){let c=((Q=a.input)==null?void 0:Q.file_path)||((ue=a.input)==null?void 0:ue.path)||void 0;h==null||h(ee({type:"tool_use",jobId:n,tool:a.name},c?{file:c}:{}),n),a.name==="Edit"&&((de=a.input)!=null&&de.file_path)?V.push({tool:"Edit",file_path:a.input.file_path,old_string:a.input.old_string,new_string:a.input.new_string,replace_all:a.input.replace_all}):a.name==="Write"&&((ce=a.input)!=null&&ce.file_path)&&V.push({tool:"Write",file_path:a.input.file_path,content:a.input.content})}a.type==="thinking"&&a.thinking&&(h==null||h({type:"thinking",jobId:n,text:a.thinking},n))}B.type==="user"&&((he=(pe=B.tool_use_result)==null?void 0:pe.file)!=null&&he.filePath)&&(h==null||h({type:"tool_use",jobId:n,tool:"Read",file:B.tool_use_result.file.filePath},n))}catch(B){}});let Z=[];(T=x.stderr)==null||T.on("data",A=>{Z.push(A.toString())}),x.on("close",A=>{q.close(),A!==0&&A!==null&&(X=!0,K=Z.join("")||`Claude process exited with code ${A}`),L({sessionId:Y,text:z.join(""),success:!X,error:X?K:void 0,fileEdits:V.length>0?V:void 0})}),x.on("error",A=>{X=!0,K=A.message,L({sessionId:Y,text:z.join(""),success:!1,error:K,fileEdits:V.length>0?V:void 0})})});return{process:x,result:G}}import{spawn as Qe}from"child_process";import{createInterface as We}from"readline";function ke(n,e){let{prompt:s,projectRoot:t,screenshotPath:r,resumeSessionId:l,model:i,onEvent:u}=e,m=[];l?(m.push("exec","resume",l),i&&m.push("-m",i),m.push("--json","--full-auto",s),r&&m.push("--image",r)):(m.push("exec","--json","--full-auto"),i&&m.push("-m",i),m.push(s),r&&m.push("--image",r));let _=Qe("codex",m,{cwd:t,stdio:["ignore","pipe","pipe"],env:ee({},process.env)}),h=new Promise(U=>{var K;let x,G=[],L=!1,Y="",z=We({input:_.stdout}),V=new Set;z.on("line",q=>{var R,Z,T,A;if(q.trim())try{let y=JSON.parse(q),H=(R=y.type)!=null?R:"unknown";if(V.add(H),H==="thread.started"&&y.thread_id&&!x&&(x=y.thread_id),H==="item/agentMessage/delta"&&((Z=y.delta)!=null&&Z.text)&&(G.push(y.delta.text),u==null||u({type:"delta",jobId:n,text:y.delta.text},n)),H==="item/reasoning/delta"&&((T=y.delta)!=null&&T.text)&&(u==null||u({type:"thinking",jobId:n,text:y.delta.text},n)),H==="item/started"&&y.item){let C=y.item.type;if(C==="command_execution")u==null||u({type:"tool_use",jobId:n,tool:"Bash"},n);else if(C==="file_change"){let Q=y.item.filename||y.item.path;u==null||u(ee({type:"tool_use",jobId:n,tool:"Edit"},Q?{file:Q}:{}),n)}else if(C==="file_read"){let Q=y.item.filename||y.item.path;u==null||u(ee({type:"tool_use",jobId:n,tool:"Read"},Q?{file:Q}:{}),n)}else if(C==="web_search")u==null||u({type:"tool_use",jobId:n,tool:"WebSearch"},n);else if(C==="mcp_tool_call"){let Q=y.item.tool_name||y.item.name||"MCP";u==null||u({type:"tool_use",jobId:n,tool:Q},n)}}if(H==="item/completed"&&y.item){if(y.item.type==="agent_message"){let C=y.item.text;typeof C=="string"&&C&&G.push(C)}else if(y.item.type==="reasoning"){let C=y.item.text;typeof C=="string"&&C&&(u==null||u({type:"thinking",jobId:n,text:C},n))}}H==="turn.failed"&&(L=!0,Y=((A=y.error)==null?void 0:A.message)||y.message||"Turn failed")}catch(y){}});let X=[];(K=_.stderr)==null||K.on("data",q=>{X.push(q.toString())}),_.on("close",q=>{z.close(),q!==0&&q!==null&&(L=!0,Y=X.join("")||`Codex process exited with code ${q}`),U({sessionId:x,text:G.join(""),success:!L,error:L?Y:void 0})}),_.on("error",q=>{L=!0,Y=q.message,U({sessionId:x,text:G.join(""),success:!1,error:Y})})});return{process:_,result:h}}async function ae(n){let s=(n.headers["content-type"]||"").match(/boundary=(?:"([^"]+)"|([^\s;]+))/);if(!s)throw new Error("Missing multipart boundary");let t=s[1]||s[2],r=await ze(n),l=Buffer.from(`--${t}`),i=Buffer.from(`--${t}--`),u,m,_,h,U,x,G,L,Y,z,V,X=[],K=0,q=[];for(;K<r.length;){let R=r.indexOf(l,K);if(R===-1)break;let Z=R+l.length;if(r.slice(R,R+i.length).equals(i))break;let T=Z;r[T]===13&&r[T+1]===10&&(T+=2);let A=r.indexOf(`\r
2
+ \r
3
+ `,T);if(A===-1)break;let y=r.slice(T,A).toString("utf-8"),H=A+4,C=r.indexOf(l,H),Q=C!==-1?C-2:r.length;q.push({headers:y,body:r.slice(H,Q)}),K=C!==-1?C:r.length}for(let R of q){let Z=R.headers.match(/name="([^"]+)"/);if(!Z)continue;let T=Z[1];if(T==="screenshot")u=R.body;else if(T==="feedback")m=R.body.toString("utf-8");else if(T==="color")_=R.body.toString("utf-8");else if(T==="provider")h=R.body.toString("utf-8");else if(T==="model")U=R.body.toString("utf-8");else if(T==="goal")x=R.body.toString("utf-8");else if(T==="pageUrl")G=R.body.toString("utf-8");else if(T==="viewport")L=R.body.toString("utf-8");else if(T==="planId")Y=R.body.toString("utf-8");else if(T==="manifest")z=R.body.toString("utf-8");else if(T==="tasks")V=R.body.toString("utf-8");else if(T.startsWith("image-")){let A=T.split("-"),y=parseInt(A[A.length-1],10),H=A.slice(1,-1).join("-");H&&!isNaN(y)&&X.push({annotationId:H,index:y,data:R.body})}}if(!u)throw new Error("Missing screenshot field");return m||(m=""),{screenshot:u,feedback:m,color:_,provider:h,model:U,goal:x,pageUrl:G,viewport:L,planId:Y,manifest:z,tasks:V,pastedImages:X}}function ze(n){return new Promise((e,s)=>{let t=[];n.on("data",r=>t.push(r)),n.on("end",()=>e(Buffer.concat(t))),n.on("error",s)})}function ge(n,e){var t;let s=[];if(n.annotations.length>0){s.push("## Annotations");for(let r of n.annotations){let l=r.elements.map(m=>{let _=[m.selector];return m.reactComponent&&_.push(`(${m.reactComponent})`),_.join(" ")}).join(", "),i=r.instruction||"No text";s.push(`- id=${r.id} [${r.type}] ${i} \u2192 Elements: ${l||"none"}`);let u=e==null?void 0:e[r.id];if(u&&u.length>0)for(let m of u)s.push(` Attached image: use the Read tool to view ${m}`)}}if(n.styleModifications.length>0){s.push(""),s.push("## Style Changes (make permanent in source)"),s.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:");for(let r of n.styleModifications){let l=(t=r.element)!=null&&t.reactComponent?`(${r.element.reactComponent})`:"";for(let i of r.changes)s.push(`- ${r.selector} ${l}: ${i.property} ${i.original} \u2192 ${i.modified}`)}}if(n.inspectedElement){let r=n.inspectedElement;s.push(""),s.push("## Inspected Element"),s.push("The developer has this element selected in the inspector:");let l=[r.selector];r.reactComponent&&l.push(`(${r.reactComponent})`),r.context&&l.push(`in ${r.context}`),r.textContent&&l.push(`"${r.textContent.slice(0,80)}"`),s.push(`- ${l.join(" ")}`)}return s.join(`
4
+ `)}function $e(n,e,s){let t=[];if(t.push("You are reviewing a UI screenshot with developer annotations."),t.push(""),(s==null?void 0:s.provider)!=="codex"&&(t.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${n}`),t.push("")),t.push(`The developer annotated their running app at ${e.url} (${e.viewport.width}x${e.viewport.height}).`),s!=null&&s.threadHistory&&s.threadHistory.length>0){t.push(""),t.push("## Previous Conversation");let l=0;for(let i of s.threadHistory)if(i.role==="human")l++,i.replyToQuestion?(t.push(`### Round ${l} (human) \u2014 reply`),t.push(`"${i.replyToQuestion}"`)):(t.push(`### Round ${l} (human)`),i.feedbackSummary&&t.push(`Annotations: ${i.feedbackSummary}`),i.annotationIds&&i.annotationIds.length>0&&t.push(`Annotation IDs: ${i.annotationIds.join(", ")}`));else if(i.question)t.push(`### Round ${l} (assistant) \u2014 question`),t.push(`"${i.question}"`);else{if(t.push(`### Round ${l} (assistant)`),i.responseText&&t.push(`Response: ${i.responseText}`),i.resolutions&&i.resolutions.length>0)for(let u of i.resolutions)t.push(`- ${u.annotationId}: ${u.status} \u2014 ${u.summary}`),u.filesModified&&u.filesModified.length>0&&t.push(` Files: ${u.filesModified.join(", ")}`);i.toolsUsed&&i.toolsUsed.length>0&&t.push(`Tools used: ${i.toolsUsed.join(", ")}`)}t.push(""),t.push("The current round is shown in full below.")}let r=ge(e,s==null?void 0:s.imagePaths);return r&&(t.push(""),t.push(r)),t.push(""),t.push("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."),t.push(""),t.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."),t.push(""),t.push("## Resolution"),t.push("After completing all work, output a resolution block listing what you did for each annotation:"),t.push("<resolution>"),t.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]'),t.push("</resolution>"),t.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),t.push(""),t.push("## Questions"),t.push("If the annotation text is unclear, ambiguous, gibberish, or you are unsure what the developer wants, output a question:"),t.push('<question>What do you mean by "..."?</question>'),t.push("Do NOT guess what unclear instructions mean \u2014 ask instead."),t.push("You may output BOTH a <resolution> for clear annotations AND a <question> for unclear ones in the same response."),t.join(`
5
+ `)}function Re(n){var s;let e=n.match(/<question>\s*([\s\S]*?)\s*<\/question>/);return(s=e==null?void 0:e[1])!=null?s:null}function Ce(n,e,s,t){let r=[];r.push("You are continuing work on a UI based on the developer's reply to your question."),r.push(""),s!=="codex"&&r.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${n}`);let l=e.find(i=>i.role==="human"&&i.feedbackContext);if(l!=null&&l.feedbackContext&&(r.push(""),r.push(l.feedbackContext)),e.length>0){r.push(""),r.push("## Conversation History");let i=0;for(let u of e)u.role==="human"?(i++,u.replyToQuestion?(r.push(`### Round ${i} (human) \u2014 reply`),r.push(`"${u.replyToQuestion}"`)):(r.push(`### Round ${i} (human)`),u.feedbackSummary&&r.push(`Annotations: ${u.feedbackSummary}`))):u.question?(r.push(`### Round ${i} (assistant) \u2014 question`),r.push(`"${u.question}"`)):(r.push(`### Round ${i} (assistant)`),u.responseText&&r.push(`Response: ${u.responseText}`))}if(r.push(""),r.push("The developer answered your question. Continue working based on their reply."),r.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately."),r.push(""),r.push("IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."),t&&t.length>0){r.push(""),r.push("## Attached Images"),r.push("The developer attached reference images with their reply:");for(let i of t)r.push(`Attached image: use the Read tool to view the image at: ${i}`)}return r.push(""),r.push("## Resolution"),r.push("After completing all work, output a resolution block listing what you did for each annotation:"),r.push("<resolution>"),r.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]'),r.push("</resolution>"),r.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),r.push(""),r.push("## Questions"),r.push("If you still need clarification, output:"),r.push("<question>Your question here</question>"),r.push("You may output BOTH a <resolution> and a <question> in the same response."),r.join(`
6
+ `)}function Oe(n){return typeof n=="object"&&n!==null&&typeof n.annotationId=="string"&&(n.status==="resolved"||n.status==="needs_review")&&typeof n.summary=="string"}function _e(n){let e=n.match(/<resolution>\s*([\s\S]*?)\s*<\/resolution>/);if(!e||!e[1])return[];try{let s=JSON.parse(e[1]);return Array.isArray(s)?s.filter(Oe):[]}catch(s){return[]}}function Ae(n){let e=[],s=/<resolution>\s*([\s\S]*?)\s*<\/resolution>/g,t;for(;(t=s.exec(n))!==null;)if(t[1])try{let r=JSON.parse(t[1]);Array.isArray(r)&&e.push(...r.filter(Oe))}catch(r){}return e}function Ee(n,e,s,t,r,l){let i=[];return i.push("You are a UI design planner. You are looking at a full-page screenshot of a web application."),i.push(""),i.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${n}`),i.push(""),i.push(`Page: ${s}`),i.push(`Viewport: ${t.width}x${t.height}`),r&&(i.push(""),i.push("## Page Elements (ground truth)"),i.push("Below is a structured inventory of actual DOM elements on this page. Cross-reference"),i.push("against this list \u2014 do NOT reference elements that aren't listed here."),i.push(""),i.push("<manifest>"),i.push(r),i.push("</manifest>")),l&&(i.push(""),i.push("## Developer Context"),i.push("The developer has the following annotations and style changes on their canvas. Factor these into your plan:"),i.push(l)),i.push(""),i.push("## Goal"),i.push(e),i.push(""),i.push("## Your Task"),i.push("Analyze the screenshot and decompose the goal into specific, element-level tasks."),i.push("Each task targets a specific region of the page and gives a clear instruction for a worker agent."),i.push(""),i.push("Output your plan as a JSON array inside a <plan> tag. Each task has:"),i.push('- `id`: A short unique identifier (e.g., "t1", "t2")'),i.push("- `instruction`: Clear, specific instruction for a worker agent (what to change and how)"),i.push("- `region`: Bounding box in page coordinates `{x, y, width, height}` \u2014 where (x,y) is top-left corner"),i.push("- `priority`: Optional 1-5 (1=highest). Tasks with no dependency can share a priority level."),i.push(""),i.push("Example:"),i.push("<plan>"),i.push("["),i.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},'),i.push(' {"id":"t2","instruction":"Add a subtle box-shadow to the card container","region":{"x":80,"y":200,"width":640,"height":300},"priority":2}'),i.push("]"),i.push("</plan>"),i.push(""),i.push("Guidelines:"),i.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."),i.push('- Be specific about values (colors, sizes, spacing) rather than vague ("make it look better")'),i.push("- Each task should be independently actionable by a worker that can only see its region"),i.push("- Regions should tightly bound the relevant UI element(s)"),i.push("- Keep tasks atomic \u2014 one change per task, not multiple unrelated changes"),i.push("- Order by priority: structural changes first, then visual polish"),i.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."),i.push("- If the goal is unclear or you need more context, output a question instead:"),i.push("<question>Your question here</question>"),i.push(""),i.push("Do NOT modify any files. You are a planner only \u2014 output a <plan> or <question>, nothing else."),i.join(`
7
+ `)}function Me(n){let e=n.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);if(!(e!=null&&e[1]))return null;try{let s=JSON.parse(e[1]);return Array.isArray(s)?s.filter(t=>{if(typeof t!="object"||t===null)return!1;let r=t;if(typeof r.id!="string"||typeof r.instruction!="string"||typeof r.region!="object"||r.region===null)return!1;let l=r.region;return typeof l.x=="number"&&typeof l.y=="number"&&typeof l.width=="number"&&typeof l.height=="number"}):null}catch(s){return null}}function Je(n,e,s){let t=[];t.push("You are reviewing whether a series of UI changes achieved the original design goal."),t.push(""),t.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${n}`),t.push(""),t.push("## Original Goal"),t.push(e),t.push(""),t.push("## Completed Tasks");for(let r of s)t.push(`- [${r.id}] ${r.instruction} \u2192 ${r.summary}`);return t.push(""),t.push("## Your Task"),t.push("Look at the current screenshot and determine if the goal has been achieved."),t.push("Output your verdict inside a <review> tag:"),t.push("<review>"),t.push('{"verdict":"pass","summary":"The changes look good..."}'),t.push("</review>"),t.push(""),t.push("Or if issues remain:"),t.push("<review>"),t.push('{"verdict":"fail","summary":"Some issues remain...","issues":["Issue 1","Issue 2"]}'),t.push("</review>"),t.push(""),t.push("Do NOT modify any files. Output only a <review> block."),t.join(`
8
+ `)}function Ne(n,e,s,t,r){let l=[];l.push("You are implementing a series of UI changes on a web application."),l.push(""),r!=="codex"&&(l.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${n}`),l.push("")),l.push(`Page: ${s} (${t.width}x${t.height})`),l.push(""),l.push("## Tasks"),l.push("Each task targets a specific region of the page. Complete them in order."),l.push("");for(let i of e){if(l.push(`### Task ${i.planTaskId} (annotationId: ${i.annotationId})`),l.push(`Instruction: ${i.instruction}`),l.push(`Region: (${i.region.x}, ${i.region.y}) ${i.region.width}x${i.region.height}`),i.linkedSelector&&l.push(`Target element: ${i.linkedSelector}`),i.elements&&i.elements.length>0){let u=i.elements.map(m=>{let _=[m.selector];return m.reactComponent&&_.push(`(${m.reactComponent})`),_.join(" ")}).join(", ");l.push(`Elements: ${u}`)}l.push("")}return l.push("## Instructions"),l.push("- Apply each change to the source files \u2014 the dev server has HMR so changes appear immediately."),l.push("- IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source."),l.push("- You may use parallel subagents (Task tool) for independent changes, or work serially \u2014 use your judgment."),l.push(""),l.push("## Resolution"),l.push("CRITICAL: After completing EACH task, immediately output a <resolution> block for that task."),l.push("Do NOT wait until all tasks are done \u2014 output each resolution as soon as that task is finished."),l.push("<resolution>"),l.push('[{"annotationId":"<annotationId>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]'),l.push("</resolution>"),l.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`),l.join(`
9
+ `)}function De(n){let e=n.match(/<review>\s*([\s\S]*?)\s*<\/review>/);if(!(e!=null&&e[1]))return null;try{let s=JSON.parse(e[1]);return typeof s!="object"||s===null||s.verdict!=="pass"&&s.verdict!=="fail"||typeof s.summary!="string"?null:{verdict:s.verdict,summary:s.summary,issues:Array.isArray(s.issues)?s.issues.filter(t=>typeof t=="string"):void 0}}catch(s){return null}}var ye=class{constructor(e=5){this.queue=[];this.activeJobs=new Map;this.activeProcesses=new Map;this.listeners=new Set;this.processor=null;this.maxConcurrency=e}setProcessor(e){this.processor=e}get active(){let e=this.activeJobs.values().next();return e.done?null:e.value}get allActive(){return Array.from(this.activeJobs.values())}get activeCount(){return this.activeJobs.size}get depth(){return this.queue.length}get isRunning(){return this.activeJobs.size>0}setActiveProcess(e,s){s?this.activeProcesses.set(e,s):this.activeProcesses.delete(e)}enqueue(e){return this.queue.push(e),this.processNext(),this.queue.length+this.activeJobs.size}addListener(e){return this.listeners.add(e),()=>this.listeners.delete(e)}broadcast(e,s){for(let t of this.listeners)t(e,s)}cancelJob(e){let s=this.activeProcesses.get(e),t=this.activeJobs.get(e);return!s||!t?!1:(s.kill("SIGTERM"),this.activeProcesses.delete(e),this.activeJobs.delete(e),t.status="error",t.error="Cancelled by user",this.broadcast({type:"error",jobId:t.id,message:"Cancelled by user"},t.id),this.processNext(),!0)}cancelActive(){if(this.activeJobs.size===0)return!1;let e=Array.from(this.activeJobs.keys());for(let s of e)this.cancelJob(s);return!0}destroy(){for(let e of this.activeProcesses.values())e.kill("SIGTERM");this.activeProcesses.clear(),this.activeJobs.clear(),this.queue=[],this.listeners.clear()}processNext(){for(;this.activeJobs.size<this.maxConcurrency&&this.queue.length>0&&this.processor;){let e=this.queue.shift();this.activeJobs.set(e.id,e),e.status="running",this.broadcast({type:"job_started",jobId:e.id,position:0},e.id),this.processor(e).catch(s=>{e.status="error",e.error=s instanceof Error?s.message:String(s),this.broadcast({type:"error",jobId:e.id,message:e.error},e.id)}).finally(()=>{this.activeJobs.delete(e.id),this.activeProcesses.delete(e.id),this.processNext(),this.activeJobs.size===0&&this.queue.length===0&&this.broadcast({type:"queue_drained"},e.id)})}}};import{mkdir as Ve,readFile as Ke,writeFile as Xe}from"fs/promises";import{dirname as Ze,join as je}from"path";var et={version:1,threads:{}},we=class{constructor(e){this.cache=null;this.writeChain=Promise.resolve();this.filePath=je(e,".popmelt","threads.json")}async load(){if(this.cache)return this.cache;try{let e=await Ke(this.filePath,"utf-8"),s=JSON.parse(e);if(s&&s.version===1&&s.threads)return this.cache=s,this.cache}catch(e){}return this.cache=ie(ee({},et),{threads:{}}),this.cache}async getThread(e){var t;return(t=(await this.load()).threads[e])!=null?t:null}async findContinuationThread(e){if(e.length===0)return null;let s=await this.load(),t=new Set(e);for(let r of Object.values(s.threads))if(r.elementIdentifiers.some(i=>t.has(i)))return r;return null}async createThread(e,s){let t=await this.load(),r={id:e,createdAt:Date.now(),updatedAt:Date.now(),elementIdentifiers:s,messages:[]};return t.threads[e]=r,await this.persist(),r}async appendMessage(e,s){let r=(await this.load()).threads[e];r&&(r.messages.push(s),r.updatedAt=Date.now(),await this.persist())}async addElementIdentifiers(e,s){let r=(await this.load()).threads[e];if(!r)return;let l=new Set(r.elementIdentifiers);for(let i of s)l.has(i)||r.elementIdentifiers.push(i);r.updatedAt=Date.now(),await this.persist()}async getThreadHistory(e,s=6){let t=await this.getThread(e);return!t||t.messages.length===0?[]:t.messages.length<=s?t.messages:[t.messages[0],...t.messages.slice(-(s-1))]}async persist(){this.writeChain=this.writeChain.then(async()=>{if(this.cache)try{await Ve(Ze(this.filePath),{recursive:!0}),await Xe(this.filePath,JSON.stringify(this.cache,null,2))}catch(e){console.error("[ThreadStore] Failed to persist:",e)}}),await this.writeChain}};var lt=1111,ut=["Read","Edit","Write","Glob","Grep","Bash","WebFetch","WebSearch","Bash(curl:*)"],dt=1800*1e3,ct=3600*1e3;function pt(n){if(!n)return!1;try{let e=new URL(n);return e.hostname==="localhost"||e.hostname==="127.0.0.1"}catch(e){return!1}}function ht(n,e){let s=n.headers.origin;pt(s)&&(e.setHeader("Access-Control-Allow-Origin",s),e.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),e.setHeader("Access-Control-Allow-Headers","Content-Type"))}function I(n,e,s){n.writeHead(e,{"Content-Type":"application/json"}),n.end(JSON.stringify(s))}function ft(n,e){if(!n)return e;let s=n.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);if(!s)return e;let[,t,r,l]=s;return`\x1B[38;2;${parseInt(t,16)};${parseInt(r,16)};${parseInt(l,16)}m${e}\x1B[0m`}function gt(n,e){try{n.res.write(`event: ${e.type}
10
+ data: ${JSON.stringify(e)}
34
11
 
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";
12
+ `)}catch(s){}}async function Be(n={}){var C,Q,ue,de,ce,pe,he,B;let e=(C=n.port)!=null?C:lt,s=(Q=n.projectRoot)!=null?Q:process.cwd(),t=(ue=n.tempDir)!=null?ue:oe(at(),"popmelt-bridge"),r=(de=n.maxTurns)!=null?de:10,l=(ce=n.maxBudgetUsd)!=null?ce:1,i=(pe=n.allowedTools)!=null?pe:ut,u=(he=n.claudePath)!=null?he:"claude",m=(B=n.provider)!=null?B:"claude",_={};for(let o of["claude","codex"])try{let a=tt("which",[o],{encoding:"utf-8"}).trim();_[o]={available:!0,path:a}}catch(a){_[o]={available:!1,path:null}}await st(t,{recursive:!0}),Ue(t).catch(()=>{});let h=new ye,U=new Set,x=new we(s),G=new Map;h.addListener((o,a)=>{for(let c of U)gt(c,o)}),h.setProcessor(async o=>{var M,W,te,b,j,se,fe;let a=o._replyPrompt,c=(M=o.provider)!=null?M:m,d;if(o.threadId){let v=await x.getThread(o.threadId);if(v){for(let D=v.messages.length-1;D>=0;D--)if(v.messages[D].sessionId){d=v.messages[D].sessionId;break}}}let g;if(d&&a){let v=(W=await x.getThread(o.threadId))==null?void 0:W.messages.filter(P=>P.role==="human").pop();g=((v==null?void 0:v.replyToQuestion)||(v==null?void 0:v.feedbackSummary)||"")+`
42
13
 
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
- }
14
+ After completing work, output a <resolution> block. If unclear, output a <question> block.`}else if(d)g=ge(o.feedback,o.imagePaths)+`
178
15
 
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
- }
16
+ Follow the developer's instructions. If they ask for changes, apply them to the source files.
304
17
 
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
- const pastedImages = [];
328
- let offset = 0;
329
- const parts = [];
330
- while (offset < body.length) {
331
- const delimStart = body.indexOf(delimiter, offset);
332
- if (delimStart === -1) break;
333
- const afterDelim = delimStart + delimiter.length;
334
- if (body.slice(delimStart, delimStart + endDelimiter.length).equals(endDelimiter)) {
335
- break;
336
- }
337
- let headerStart = afterDelim;
338
- if (body[headerStart] === 13 && body[headerStart + 1] === 10) {
339
- headerStart += 2;
340
- }
341
- const headerEnd = body.indexOf("\r\n\r\n", headerStart);
342
- if (headerEnd === -1) break;
343
- const headers = body.slice(headerStart, headerEnd).toString("utf-8");
344
- const bodyStart = headerEnd + 4;
345
- const nextDelim = body.indexOf(delimiter, bodyStart);
346
- const bodyEnd = nextDelim !== -1 ? nextDelim - 2 : body.length;
347
- parts.push({
348
- headers,
349
- body: body.slice(bodyStart, bodyEnd)
350
- });
351
- offset = nextDelim !== -1 ? nextDelim : body.length;
352
- }
353
- for (const part of parts) {
354
- const nameMatch = part.headers.match(/name="([^"]+)"/);
355
- if (!nameMatch) continue;
356
- const name = nameMatch[1];
357
- if (name === "screenshot") {
358
- screenshot = part.body;
359
- } else if (name === "feedback") {
360
- feedback = part.body.toString("utf-8");
361
- } else if (name === "color") {
362
- color = part.body.toString("utf-8");
363
- } else if (name === "provider") {
364
- provider = part.body.toString("utf-8");
365
- } else if (name === "model") {
366
- model = part.body.toString("utf-8");
367
- } else if (name === "goal") {
368
- goal = part.body.toString("utf-8");
369
- } else if (name === "pageUrl") {
370
- pageUrl = part.body.toString("utf-8");
371
- } else if (name === "viewport") {
372
- viewport = part.body.toString("utf-8");
373
- } else if (name === "planId") {
374
- planId = part.body.toString("utf-8");
375
- } else if (name === "manifest") {
376
- manifest = part.body.toString("utf-8");
377
- } else if (name === "tasks") {
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
- }
386
- }
387
- }
388
- if (!screenshot) throw new Error("Missing screenshot field");
389
- if (!feedback) feedback = "";
390
- return { screenshot, feedback, color, provider, model, goal, pageUrl, viewport, planId, manifest, tasks, pastedImages };
391
- }
392
- function readBody(req) {
393
- return new Promise((resolve, reject) => {
394
- const chunks = [];
395
- req.on("data", (chunk) => chunks.push(chunk));
396
- req.on("end", () => resolve(Buffer.concat(chunks)));
397
- req.on("error", reject);
398
- });
399
- }
18
+ After completing work, output a <resolution> block. If unclear, output a <question> block.`+(c!=="codex"?`
400
19
 
401
- // src/server/prompt-builder.ts
402
- function formatFeedbackContext(feedback, imagePaths) {
403
- var _a;
404
- const lines = [];
405
- if (feedback.annotations.length > 0) {
406
- lines.push("## Annotations");
407
- for (const ann of feedback.annotations) {
408
- const elementsDesc = ann.elements.map((el) => {
409
- const parts = [el.selector];
410
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
411
- return parts.join(" ");
412
- }).join(", ");
413
- const instruction = ann.instruction || "No text";
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
- }
421
- }
422
- }
423
- if (feedback.styleModifications.length > 0) {
424
- lines.push("");
425
- lines.push("## Style Changes (make permanent in source)");
426
- 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:");
427
- for (const mod of feedback.styleModifications) {
428
- const elementDesc = ((_a = mod.element) == null ? void 0 : _a.reactComponent) ? `(${mod.element.reactComponent})` : "";
429
- for (const change of mod.changes) {
430
- lines.push(
431
- `- ${mod.selector} ${elementDesc}: ${change.property} ${change.original} \u2192 ${change.modified}`
432
- );
433
- }
434
- }
435
- }
436
- if (feedback.inspectedElement) {
437
- const el = feedback.inspectedElement;
438
- lines.push("");
439
- lines.push("## Inspected Element");
440
- lines.push("The developer has this element selected in the inspector:");
441
- const parts = [el.selector];
442
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
443
- if (el.context) parts.push(`in ${el.context}`);
444
- if (el.textContent) parts.push(`"${el.textContent.slice(0, 80)}"`);
445
- lines.push(`- ${parts.join(" ")}`);
446
- }
447
- return lines.join("\n");
448
- }
449
- function buildPrompt(screenshotPath, feedback, options) {
450
- const lines = [];
451
- lines.push("You are reviewing a UI screenshot with developer annotations.");
452
- lines.push("");
453
- if ((options == null ? void 0 : options.provider) !== "codex") {
454
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
455
- lines.push("");
456
- }
457
- lines.push(
458
- `The developer annotated their running app at ${feedback.url} (${feedback.viewport.width}x${feedback.viewport.height}).`
459
- );
460
- if ((options == null ? void 0 : options.threadHistory) && options.threadHistory.length > 0) {
461
- lines.push("");
462
- lines.push("## Previous Conversation");
463
- let roundNum = 0;
464
- for (const msg of options.threadHistory) {
465
- if (msg.role === "human") {
466
- roundNum++;
467
- if (msg.replyToQuestion) {
468
- lines.push(`### Round ${roundNum} (human) \u2014 reply`);
469
- lines.push(`"${msg.replyToQuestion}"`);
470
- } else {
471
- lines.push(`### Round ${roundNum} (human)`);
472
- if (msg.feedbackSummary) {
473
- lines.push(`Annotations: ${msg.feedbackSummary}`);
474
- }
475
- if (msg.annotationIds && msg.annotationIds.length > 0) {
476
- lines.push(`Annotation IDs: ${msg.annotationIds.join(", ")}`);
477
- }
478
- }
479
- } else {
480
- if (msg.question) {
481
- lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
482
- lines.push(`"${msg.question}"`);
483
- } else {
484
- lines.push(`### Round ${roundNum} (assistant)`);
485
- if (msg.responseText) {
486
- lines.push(`Response: ${msg.responseText}`);
487
- }
488
- if (msg.resolutions && msg.resolutions.length > 0) {
489
- for (const r of msg.resolutions) {
490
- lines.push(`- ${r.annotationId}: ${r.status} \u2014 ${r.summary}`);
491
- if (r.filesModified && r.filesModified.length > 0) {
492
- lines.push(` Files: ${r.filesModified.join(", ")}`);
493
- }
494
- }
495
- }
496
- if (msg.toolsUsed && msg.toolsUsed.length > 0) {
497
- lines.push(`Tools used: ${msg.toolsUsed.join(", ")}`);
498
- }
499
- }
500
- }
501
- }
502
- lines.push("");
503
- lines.push("The current round is shown in full below.");
504
- }
505
- const feedbackContext = formatFeedbackContext(feedback, options == null ? void 0 : options.imagePaths);
506
- if (feedbackContext) {
507
- lines.push("");
508
- lines.push(feedbackContext);
509
- }
510
- lines.push("");
511
- lines.push(
512
- "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."
513
- );
514
- lines.push("");
515
- lines.push(
516
- "IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source. This attribute tracks annotation positions."
517
- );
518
- lines.push("");
519
- lines.push("## Resolution");
520
- lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
521
- lines.push("<resolution>");
522
- lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
523
- lines.push("</resolution>");
524
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
525
- lines.push("");
526
- lines.push("## Questions");
527
- lines.push("If the annotation text is unclear, ambiguous, gibberish, or you are unsure what the developer wants, output a question:");
528
- lines.push('<question>What do you mean by "..."?</question>');
529
- lines.push("Do NOT guess what unclear instructions mean \u2014 ask instead.");
530
- lines.push("You may output BOTH a <resolution> for clear annotations AND a <question> for unclear ones in the same response.");
531
- return lines.join("\n");
532
- }
533
- function parseQuestion(responseText) {
534
- var _a;
535
- const match = responseText.match(/<question>\s*([\s\S]*?)\s*<\/question>/);
536
- return (_a = match == null ? void 0 : match[1]) != null ? _a : null;
537
- }
538
- function buildReplyPrompt(screenshotPath, threadHistory, provider, imagePaths) {
539
- const lines = [];
540
- lines.push("You are continuing work on a UI based on the developer's reply to your question.");
541
- lines.push("");
542
- if (provider !== "codex") {
543
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
544
- }
545
- const firstHuman = threadHistory.find((m) => m.role === "human" && m.feedbackContext);
546
- if (firstHuman == null ? void 0 : firstHuman.feedbackContext) {
547
- lines.push("");
548
- lines.push(firstHuman.feedbackContext);
549
- }
550
- if (threadHistory.length > 0) {
551
- lines.push("");
552
- lines.push("## Conversation History");
553
- let roundNum = 0;
554
- for (const msg of threadHistory) {
555
- if (msg.role === "human") {
556
- roundNum++;
557
- if (msg.replyToQuestion) {
558
- lines.push(`### Round ${roundNum} (human) \u2014 reply`);
559
- lines.push(`"${msg.replyToQuestion}"`);
560
- } else {
561
- lines.push(`### Round ${roundNum} (human)`);
562
- if (msg.feedbackSummary) {
563
- lines.push(`Annotations: ${msg.feedbackSummary}`);
564
- }
565
- }
566
- } else {
567
- if (msg.question) {
568
- lines.push(`### Round ${roundNum} (assistant) \u2014 question`);
569
- lines.push(`"${msg.question}"`);
570
- } else {
571
- lines.push(`### Round ${roundNum} (assistant)`);
572
- if (msg.responseText) {
573
- lines.push(`Response: ${msg.responseText}`);
574
- }
575
- }
576
- }
577
- }
578
- }
579
- lines.push("");
580
- lines.push("The developer answered your question. Continue working based on their reply.");
581
- lines.push("Follow their instructions \u2014 apply code changes only if requested. The dev server has HMR so changes appear immediately.");
582
- lines.push("");
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
- }
592
- lines.push("");
593
- lines.push("## Resolution");
594
- lines.push("After completing all work, output a resolution block listing what you did for each annotation:");
595
- lines.push("<resolution>");
596
- lines.push('[{"annotationId":"<id>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
597
- lines.push("</resolution>");
598
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
599
- lines.push("");
600
- lines.push("## Questions");
601
- lines.push("If you still need clarification, output:");
602
- lines.push("<question>Your question here</question>");
603
- lines.push("You may output BOTH a <resolution> and a <question> in the same response.");
604
- return lines.join("\n");
605
- }
606
- function isValidResolution(r) {
607
- return typeof r === "object" && r !== null && typeof r.annotationId === "string" && (r.status === "resolved" || r.status === "needs_review") && typeof r.summary === "string";
608
- }
609
- function parseResolutions(responseText) {
610
- const match = responseText.match(/<resolution>\s*([\s\S]*?)\s*<\/resolution>/);
611
- if (!match || !match[1]) return [];
612
- try {
613
- const parsed = JSON.parse(match[1]);
614
- if (!Array.isArray(parsed)) return [];
615
- return parsed.filter(isValidResolution);
616
- } catch (e) {
617
- return [];
618
- }
619
- }
620
- function parseAllResolutions(responseText) {
621
- const results = [];
622
- const regex = /<resolution>\s*([\s\S]*?)\s*<\/resolution>/g;
623
- let match;
624
- while ((match = regex.exec(responseText)) !== null) {
625
- if (!match[1]) continue;
626
- try {
627
- const parsed = JSON.parse(match[1]);
628
- if (Array.isArray(parsed)) {
629
- results.push(...parsed.filter(isValidResolution));
630
- }
631
- } catch (e) {
632
- }
633
- }
634
- return results;
635
- }
636
- function buildPlannerPrompt(screenshotPath, goal, pageUrl, viewport, manifestJson, feedbackContext) {
637
- const lines = [];
638
- lines.push("You are a UI design planner. You are looking at a full-page screenshot of a web application.");
639
- lines.push("");
640
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
641
- lines.push("");
642
- lines.push(`Page: ${pageUrl}`);
643
- lines.push(`Viewport: ${viewport.width}x${viewport.height}`);
644
- if (manifestJson) {
645
- lines.push("");
646
- lines.push("## Page Elements (ground truth)");
647
- lines.push("Below is a structured inventory of actual DOM elements on this page. Cross-reference");
648
- lines.push("against this list \u2014 do NOT reference elements that aren't listed here.");
649
- lines.push("");
650
- lines.push("<manifest>");
651
- lines.push(manifestJson);
652
- lines.push("</manifest>");
653
- }
654
- if (feedbackContext) {
655
- lines.push("");
656
- lines.push("## Developer Context");
657
- lines.push("The developer has the following annotations and style changes on their canvas. Factor these into your plan:");
658
- lines.push(feedbackContext);
659
- }
660
- lines.push("");
661
- lines.push("## Goal");
662
- lines.push(goal);
663
- lines.push("");
664
- lines.push("## Your Task");
665
- lines.push("Analyze the screenshot and decompose the goal into specific, element-level tasks.");
666
- lines.push("Each task targets a specific region of the page and gives a clear instruction for a worker agent.");
667
- lines.push("");
668
- lines.push("Output your plan as a JSON array inside a <plan> tag. Each task has:");
669
- lines.push('- `id`: A short unique identifier (e.g., "t1", "t2")');
670
- lines.push("- `instruction`: Clear, specific instruction for a worker agent (what to change and how)");
671
- lines.push("- `region`: Bounding box in page coordinates `{x, y, width, height}` \u2014 where (x,y) is top-left corner");
672
- lines.push("- `priority`: Optional 1-5 (1=highest). Tasks with no dependency can share a priority level.");
673
- lines.push("");
674
- lines.push("Example:");
675
- lines.push("<plan>");
676
- lines.push("[");
677
- 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},');
678
- 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}');
679
- lines.push("]");
680
- lines.push("</plan>");
681
- lines.push("");
682
- lines.push("Guidelines:");
683
- 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.");
684
- lines.push('- Be specific about values (colors, sizes, spacing) rather than vague ("make it look better")');
685
- lines.push("- Each task should be independently actionable by a worker that can only see its region");
686
- lines.push("- Regions should tightly bound the relevant UI element(s)");
687
- lines.push("- Keep tasks atomic \u2014 one change per task, not multiple unrelated changes");
688
- lines.push("- Order by priority: structural changes first, then visual polish");
689
- 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.");
690
- lines.push("- If the goal is unclear or you need more context, output a question instead:");
691
- lines.push("<question>Your question here</question>");
692
- lines.push("");
693
- lines.push("Do NOT modify any files. You are a planner only \u2014 output a <plan> or <question>, nothing else.");
694
- return lines.join("\n");
695
- }
696
- function parsePlan(responseText) {
697
- const match = responseText.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);
698
- if (!(match == null ? void 0 : match[1])) return null;
699
- try {
700
- const parsed = JSON.parse(match[1]);
701
- if (!Array.isArray(parsed)) return null;
702
- return parsed.filter(
703
- (t) => {
704
- if (typeof t !== "object" || t === null) return false;
705
- const obj = t;
706
- if (typeof obj.id !== "string" || typeof obj.instruction !== "string") return false;
707
- if (typeof obj.region !== "object" || obj.region === null) return false;
708
- const r = obj.region;
709
- return typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number";
710
- }
711
- );
712
- } catch (e) {
713
- return null;
714
- }
715
- }
716
- function buildReviewerPrompt(screenshotPath, goal, completedTasks) {
717
- const lines = [];
718
- lines.push("You are reviewing whether a series of UI changes achieved the original design goal.");
719
- lines.push("");
720
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
721
- lines.push("");
722
- lines.push("## Original Goal");
723
- lines.push(goal);
724
- lines.push("");
725
- lines.push("## Completed Tasks");
726
- for (const task of completedTasks) {
727
- lines.push(`- [${task.id}] ${task.instruction} \u2192 ${task.summary}`);
728
- }
729
- lines.push("");
730
- lines.push("## Your Task");
731
- lines.push("Look at the current screenshot and determine if the goal has been achieved.");
732
- lines.push("Output your verdict inside a <review> tag:");
733
- lines.push("<review>");
734
- lines.push('{"verdict":"pass","summary":"The changes look good..."}');
735
- lines.push("</review>");
736
- lines.push("");
737
- lines.push("Or if issues remain:");
738
- lines.push("<review>");
739
- lines.push('{"verdict":"fail","summary":"Some issues remain...","issues":["Issue 1","Issue 2"]}');
740
- lines.push("</review>");
741
- lines.push("");
742
- lines.push("Do NOT modify any files. Output only a <review> block.");
743
- return lines.join("\n");
744
- }
745
- function buildPlanExecutorPrompt(screenshotPath, tasks, pageUrl, viewport, provider) {
746
- const lines = [];
747
- lines.push("You are implementing a series of UI changes on a web application.");
748
- lines.push("");
749
- if (provider !== "codex") {
750
- lines.push(`IMPORTANT: First, use the Read tool to view the screenshot at: ${screenshotPath}`);
751
- lines.push("");
752
- }
753
- lines.push(`Page: ${pageUrl} (${viewport.width}x${viewport.height})`);
754
- lines.push("");
755
- lines.push("## Tasks");
756
- lines.push("Each task targets a specific region of the page. Complete them in order.");
757
- lines.push("");
758
- for (const task of tasks) {
759
- lines.push(`### Task ${task.planTaskId} (annotationId: ${task.annotationId})`);
760
- lines.push(`Instruction: ${task.instruction}`);
761
- lines.push(`Region: (${task.region.x}, ${task.region.y}) ${task.region.width}x${task.region.height}`);
762
- if (task.linkedSelector) {
763
- lines.push(`Target element: ${task.linkedSelector}`);
764
- }
765
- if (task.elements && task.elements.length > 0) {
766
- const elemDesc = task.elements.map((el) => {
767
- const parts = [el.selector];
768
- if (el.reactComponent) parts.push(`(${el.reactComponent})`);
769
- return parts.join(" ");
770
- }).join(", ");
771
- lines.push(`Elements: ${elemDesc}`);
772
- }
773
- lines.push("");
774
- }
775
- lines.push("## Instructions");
776
- lines.push("- Apply each change to the source files \u2014 the dev server has HMR so changes appear immediately.");
777
- lines.push("- IMPORTANT: If any elements you modify have a `data-pm` attribute, preserve it in the source.");
778
- lines.push("- You may use parallel subagents (Task tool) for independent changes, or work serially \u2014 use your judgment.");
779
- lines.push("");
780
- lines.push("## Resolution");
781
- lines.push("CRITICAL: After completing EACH task, immediately output a <resolution> block for that task.");
782
- lines.push("Do NOT wait until all tasks are done \u2014 output each resolution as soon as that task is finished.");
783
- lines.push("<resolution>");
784
- lines.push('[{"annotationId":"<annotationId>","status":"resolved","summary":"<what you did>","filesModified":["<file>"]}]');
785
- lines.push("</resolution>");
786
- lines.push(`Use status "resolved" when the change is complete, or "needs_review" if you're unsure about the result.`);
787
- return lines.join("\n");
788
- }
789
- function parseReview(responseText) {
790
- const match = responseText.match(/<review>\s*([\s\S]*?)\s*<\/review>/);
791
- if (!(match == null ? void 0 : match[1])) return null;
792
- try {
793
- const parsed = JSON.parse(match[1]);
794
- if (typeof parsed !== "object" || parsed === null) return null;
795
- if (parsed.verdict !== "pass" && parsed.verdict !== "fail") return null;
796
- if (typeof parsed.summary !== "string") return null;
797
- return {
798
- verdict: parsed.verdict,
799
- summary: parsed.summary,
800
- issues: Array.isArray(parsed.issues) ? parsed.issues.filter((i) => typeof i === "string") : void 0
801
- };
802
- } catch (e) {
803
- return null;
804
- }
805
- }
806
-
807
- // src/server/queue.ts
808
- var JobQueue = class {
809
- constructor(maxConcurrency = 5) {
810
- this.queue = [];
811
- this.activeJobs = /* @__PURE__ */ new Map();
812
- this.activeProcesses = /* @__PURE__ */ new Map();
813
- this.listeners = /* @__PURE__ */ new Set();
814
- this.processor = null;
815
- this.maxConcurrency = maxConcurrency;
816
- }
817
- setProcessor(fn) {
818
- this.processor = fn;
819
- }
820
- /** First active job (backward compat for status endpoint) */
821
- get active() {
822
- const first = this.activeJobs.values().next();
823
- return first.done ? null : first.value;
824
- }
825
- get allActive() {
826
- return Array.from(this.activeJobs.values());
827
- }
828
- get activeCount() {
829
- return this.activeJobs.size;
830
- }
831
- get depth() {
832
- return this.queue.length;
833
- }
834
- get isRunning() {
835
- return this.activeJobs.size > 0;
836
- }
837
- setActiveProcess(jobId, proc) {
838
- if (proc) {
839
- this.activeProcesses.set(jobId, proc);
840
- } else {
841
- this.activeProcesses.delete(jobId);
842
- }
843
- }
844
- enqueue(job) {
845
- this.queue.push(job);
846
- this.processNext();
847
- return this.queue.length + this.activeJobs.size;
848
- }
849
- addListener(listener) {
850
- this.listeners.add(listener);
851
- return () => this.listeners.delete(listener);
852
- }
853
- broadcast(event, jobId) {
854
- for (const listener of this.listeners) {
855
- listener(event, jobId);
856
- }
857
- }
858
- cancelJob(jobId) {
859
- const proc = this.activeProcesses.get(jobId);
860
- const job = this.activeJobs.get(jobId);
861
- if (!proc || !job) return false;
862
- proc.kill("SIGTERM");
863
- this.activeProcesses.delete(jobId);
864
- this.activeJobs.delete(jobId);
865
- job.status = "error";
866
- job.error = "Cancelled by user";
867
- this.broadcast(
868
- { type: "error", jobId: job.id, message: "Cancelled by user" },
869
- job.id
870
- );
871
- this.processNext();
872
- return true;
873
- }
874
- cancelActive() {
875
- if (this.activeJobs.size === 0) return false;
876
- const jobIds = Array.from(this.activeJobs.keys());
877
- for (const jobId of jobIds) {
878
- this.cancelJob(jobId);
879
- }
880
- return true;
881
- }
882
- destroy() {
883
- for (const proc of this.activeProcesses.values()) {
884
- proc.kill("SIGTERM");
885
- }
886
- this.activeProcesses.clear();
887
- this.activeJobs.clear();
888
- this.queue = [];
889
- this.listeners.clear();
890
- }
891
- processNext() {
892
- while (this.activeJobs.size < this.maxConcurrency && this.queue.length > 0 && this.processor) {
893
- const job = this.queue.shift();
894
- this.activeJobs.set(job.id, job);
895
- job.status = "running";
896
- this.broadcast({ type: "job_started", jobId: job.id, position: 0 }, job.id);
897
- this.processor(job).catch((err) => {
898
- job.status = "error";
899
- job.error = err instanceof Error ? err.message : String(err);
900
- this.broadcast(
901
- { type: "error", jobId: job.id, message: job.error },
902
- job.id
903
- );
904
- }).finally(() => {
905
- this.activeJobs.delete(job.id);
906
- this.activeProcesses.delete(job.id);
907
- this.processNext();
908
- if (this.activeJobs.size === 0 && this.queue.length === 0) {
909
- this.broadcast({ type: "queue_drained" }, job.id);
910
- }
911
- });
912
- }
913
- }
914
- };
915
-
916
- // src/server/thread-store.ts
917
- import { mkdir, readFile, writeFile } from "fs/promises";
918
- import { dirname, join } from "path";
919
- var EMPTY_STORE = { version: 1, threads: {} };
920
- var ThreadFileStore = class {
921
- constructor(projectRoot) {
922
- this.cache = null;
923
- this.writeChain = Promise.resolve();
924
- this.filePath = join(projectRoot, ".popmelt", "threads.json");
925
- }
926
- async load() {
927
- if (this.cache) return this.cache;
928
- try {
929
- const raw = await readFile(this.filePath, "utf-8");
930
- const parsed = JSON.parse(raw);
931
- if (parsed && parsed.version === 1 && parsed.threads) {
932
- this.cache = parsed;
933
- return this.cache;
934
- }
935
- } catch (e) {
936
- }
937
- this.cache = __spreadProps(__spreadValues({}, EMPTY_STORE), { threads: {} });
938
- return this.cache;
939
- }
940
- async getThread(id) {
941
- var _a;
942
- const store = await this.load();
943
- return (_a = store.threads[id]) != null ? _a : null;
944
- }
945
- async findContinuationThread(linkedSelectors) {
946
- if (linkedSelectors.length === 0) return null;
947
- const store = await this.load();
948
- const selectorSet = new Set(linkedSelectors);
949
- for (const thread of Object.values(store.threads)) {
950
- const hasOverlap = thread.elementIdentifiers.some((id) => selectorSet.has(id));
951
- if (hasOverlap) return thread;
952
- }
953
- return null;
954
- }
955
- async createThread(id, linkedSelectors) {
956
- const store = await this.load();
957
- const thread = {
958
- id,
959
- createdAt: Date.now(),
960
- updatedAt: Date.now(),
961
- elementIdentifiers: linkedSelectors,
962
- messages: []
963
- };
964
- store.threads[id] = thread;
965
- await this.persist();
966
- return thread;
967
- }
968
- async appendMessage(threadId, message) {
969
- const store = await this.load();
970
- const thread = store.threads[threadId];
971
- if (!thread) return;
972
- thread.messages.push(message);
973
- thread.updatedAt = Date.now();
974
- await this.persist();
975
- }
976
- async addElementIdentifiers(threadId, selectors) {
977
- const store = await this.load();
978
- const thread = store.threads[threadId];
979
- if (!thread) return;
980
- const existing = new Set(thread.elementIdentifiers);
981
- for (const sel of selectors) {
982
- if (!existing.has(sel)) {
983
- thread.elementIdentifiers.push(sel);
984
- }
985
- }
986
- thread.updatedAt = Date.now();
987
- await this.persist();
988
- }
989
- async getThreadHistory(threadId, maxMessages = 6) {
990
- const thread = await this.getThread(threadId);
991
- if (!thread || thread.messages.length === 0) return [];
992
- if (thread.messages.length <= maxMessages) {
993
- return thread.messages;
994
- }
995
- return [
996
- thread.messages[0],
997
- ...thread.messages.slice(-(maxMessages - 1))
998
- ];
999
- }
1000
- async persist() {
1001
- this.writeChain = this.writeChain.then(async () => {
1002
- if (!this.cache) return;
1003
- try {
1004
- await mkdir(dirname(this.filePath), { recursive: true });
1005
- await writeFile(this.filePath, JSON.stringify(this.cache, null, 2));
1006
- } catch (err) {
1007
- console.error("[ThreadStore] Failed to persist:", err);
1008
- }
1009
- });
1010
- await this.writeChain;
1011
- }
1012
- };
1013
-
1014
- // src/server/bridge-server.ts
1015
- var DEFAULT_PORT = 1111;
1016
- var DEFAULT_ALLOWED_TOOLS = ["Read", "Edit", "Write", "Glob", "Grep", "Bash", "WebFetch", "WebSearch", "Bash(curl:*)"];
1017
- var CLEANUP_INTERVAL_MS = 30 * 60 * 1e3;
1018
- var MAX_FILE_AGE_MS = 60 * 60 * 1e3;
1019
- function isLocalhostOrigin(origin) {
1020
- if (!origin) return false;
1021
- try {
1022
- const url = new URL(origin);
1023
- return url.hostname === "localhost" || url.hostname === "127.0.0.1";
1024
- } catch (e) {
1025
- return false;
1026
- }
1027
- }
1028
- function setCors(req, res) {
1029
- const origin = req.headers.origin;
1030
- if (isLocalhostOrigin(origin)) {
1031
- res.setHeader("Access-Control-Allow-Origin", origin);
1032
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1033
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1034
- }
1035
- }
1036
- function sendJson(res, status, data) {
1037
- res.writeHead(status, { "Content-Type": "application/json" });
1038
- res.end(JSON.stringify(data));
1039
- }
1040
- function ansiColor(hex, text) {
1041
- if (!hex) return text;
1042
- const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
1043
- if (!m) return text;
1044
- const [, r, g, b] = m;
1045
- return `\x1B[38;2;${parseInt(r, 16)};${parseInt(g, 16)};${parseInt(b, 16)}m${text}\x1B[0m`;
1046
- }
1047
- function sendSSE(client, event) {
1048
- try {
1049
- client.res.write(`event: ${event.type}
1050
- data: ${JSON.stringify(event)}
1051
-
1052
- `);
1053
- } catch (e) {
1054
- }
1055
- }
1056
- async function createPopmelt(options = {}) {
1057
- var _a, _b, _c, _d, _e, _f, _g, _h;
1058
- const port = (_a = options.port) != null ? _a : DEFAULT_PORT;
1059
- const projectRoot = (_b = options.projectRoot) != null ? _b : process.cwd();
1060
- const tempDir = (_c = options.tempDir) != null ? _c : join2(tmpdir(), "popmelt-bridge");
1061
- const maxTurns = (_d = options.maxTurns) != null ? _d : 10;
1062
- const maxBudgetUsd = (_e = options.maxBudgetUsd) != null ? _e : 1;
1063
- const allowedTools = (_f = options.allowedTools) != null ? _f : DEFAULT_ALLOWED_TOOLS;
1064
- const claudePath = (_g = options.claudePath) != null ? _g : "claude";
1065
- const defaultProvider = (_h = options.provider) != null ? _h : "claude";
1066
- const capabilities = {};
1067
- for (const cli of ["claude", "codex"]) {
1068
- try {
1069
- const cliPath = execFileSync("which", [cli], { encoding: "utf-8" }).trim();
1070
- capabilities[cli] = { available: true, path: cliPath };
1071
- } catch (e) {
1072
- capabilities[cli] = { available: false, path: null };
1073
- }
1074
- }
1075
- await mkdir2(tempDir, { recursive: true });
1076
- cleanupTempDir(tempDir).catch(() => {
1077
- });
1078
- const queue = new JobQueue();
1079
- const sseClients = /* @__PURE__ */ new Set();
1080
- const threadStore = new ThreadFileStore(projectRoot);
1081
- const jobGroups = /* @__PURE__ */ new Map();
1082
- queue.addListener((event, _jobId) => {
1083
- for (const client of sseClients) {
1084
- sendSSE(client, event);
1085
- }
1086
- });
1087
- queue.setProcessor(async (job) => {
1088
- var _a2, _b2, _c2, _d2, _e2, _f2, _g2;
1089
- const replyPrompt = job._replyPrompt;
1090
- const provider = (_a2 = job.provider) != null ? _a2 : defaultProvider;
1091
- let resumeSessionId;
1092
- if (job.threadId) {
1093
- const thread = await threadStore.getThread(job.threadId);
1094
- if (thread) {
1095
- for (let i = thread.messages.length - 1; i >= 0; i--) {
1096
- if (thread.messages[i].sessionId) {
1097
- resumeSessionId = thread.messages[i].sessionId;
1098
- break;
1099
- }
1100
- }
1101
- }
1102
- }
1103
- let prompt;
1104
- if (resumeSessionId && replyPrompt) {
1105
- const lastReply = (_b2 = await threadStore.getThread(job.threadId)) == null ? void 0 : _b2.messages.filter((m) => m.role === "human").pop();
1106
- const replyText = (lastReply == null ? void 0 : lastReply.replyToQuestion) || (lastReply == null ? void 0 : lastReply.feedbackSummary) || "";
1107
- prompt = replyText + "\n\nAfter completing work, output a <resolution> block. If unclear, output a <question> block.";
1108
- } else if (resumeSessionId) {
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" ? `
1110
-
1111
- IMPORTANT: First, use the Read tool to view the updated screenshot at: ${job.screenshotPath}` : "");
1112
- } else {
1113
- const threadHistory = !replyPrompt && job.threadId ? await threadStore.getThreadHistory(job.threadId) : void 0;
1114
- prompt = replyPrompt != null ? replyPrompt : buildPrompt(job.screenshotPath, job.feedback, {
1115
- threadHistory: threadHistory && threadHistory.length > 0 ? threadHistory : void 0,
1116
- provider,
1117
- imagePaths: job.imagePaths
1118
- });
1119
- }
1120
- const tag = ansiColor(job.color, `[\u22B9 ${port}:${job.id}]`);
1121
- console.log(`${tag} Reviewing feedback ${job.screenshotPath} (provider: ${provider})${job.threadId ? ` (thread: ${job.threadId})` : ""}${resumeSessionId ? ` (resuming: ${resumeSessionId.slice(0, 8)})` : ""}`);
1122
- const isPlanExecutor = !!job._isPlanExecutor;
1123
- let deltaBuffer = "";
1124
- let lastResolutionCount = 0;
1125
- const onEvent = (event, jobId) => {
1126
- queue.broadcast(event, jobId);
1127
- if (isPlanExecutor && event.type === "delta" && "text" in event) {
1128
- deltaBuffer += event.text;
1129
- const resolutions = parseAllResolutions(deltaBuffer);
1130
- if (resolutions.length > lastResolutionCount) {
1131
- const newResolutions = resolutions.slice(lastResolutionCount);
1132
- lastResolutionCount = resolutions.length;
1133
- queue.broadcast(
1134
- { type: "task_resolved", jobId, planId: job.planId, resolutions: newResolutions, threadId: job.threadId },
1135
- jobId
1136
- );
1137
- }
1138
- }
1139
- };
1140
- const jobAllowedTools = (_c2 = job._allowedTools) != null ? _c2 : allowedTools;
1141
- const { process: proc, result } = provider === "codex" ? spawnCodex(job.id, {
1142
- prompt,
1143
- projectRoot,
1144
- screenshotPath: job.screenshotPath,
1145
- resumeSessionId,
1146
- model: job.model,
1147
- onEvent
1148
- }) : spawnClaude(job.id, {
1149
- prompt,
1150
- projectRoot,
1151
- maxTurns,
1152
- maxBudgetUsd,
1153
- allowedTools: jobAllowedTools,
1154
- claudePath,
1155
- resumeSessionId,
1156
- model: job.model,
1157
- onEvent
1158
- });
1159
- queue.setActiveProcess(job.id, proc);
1160
- const spawnResult = await result;
1161
- job.result = spawnResult.text;
1162
- if (spawnResult.success) {
1163
- console.log(`${tag} Iteration complete`);
1164
- if (spawnResult.fileEdits && spawnResult.fileEdits.length > 0) {
1165
- console.log(`${tag} Captured ${spawnResult.fileEdits.length} file edit(s): ${spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path}`).join(", ")}`);
1166
- }
1167
- job.status = "done";
1168
- const question = parseQuestion(spawnResult.text);
1169
- let resolutions = parseResolutions(spawnResult.text);
1170
- if (resolutions.length > 0 && job.annotationIds && job.annotationIds.length > 0) {
1171
- const realIdSet = new Set(job.annotationIds);
1172
- const allMatch = resolutions.every((r) => realIdSet.has(r.annotationId));
1173
- if (!allMatch) {
1174
- resolutions = resolutions.map((r, i) => __spreadProps(__spreadValues({}, r), {
1175
- annotationId: job.annotationIds[i % job.annotationIds.length]
1176
- }));
1177
- }
1178
- }
1179
- const toolsUsed = spawnResult.fileEdits && spawnResult.fileEdits.length > 0 ? spawnResult.fileEdits.map((e) => `${e.tool} ${e.file_path.split("/").pop()}`) : void 0;
1180
- if (job.threadId) {
1181
- await threadStore.appendMessage(job.threadId, {
1182
- role: "assistant",
1183
- timestamp: Date.now(),
1184
- jobId: job.id,
1185
- responseText: spawnResult.text,
1186
- resolutions: resolutions.length > 0 ? resolutions : void 0,
1187
- question: question != null ? question : void 0,
1188
- sessionId: spawnResult.sessionId,
1189
- toolsUsed
1190
- });
1191
- }
1192
- if (job.planId && !job.planTaskId) {
1193
- const group = jobGroups.get(job.planId);
1194
- if (group) {
1195
- const plan = parsePlan(spawnResult.text);
1196
- if (plan && plan.length > 0) {
1197
- group.plan = plan;
1198
- group.status = "awaiting_approval";
1199
- group.plannerThreadId = job.threadId;
1200
- console.log(`${tag} Plan ready: ${plan.length} tasks for group ${job.planId}`);
1201
- queue.broadcast(
1202
- { type: "plan_ready", jobId: job.id, planId: job.planId, tasks: plan, threadId: job.threadId },
1203
- job.id
1204
- );
1205
- } else if (!question) {
1206
- group.status = "error";
1207
- console.error(`${tag} Failed to parse plan from planner response`);
1208
- }
1209
- }
1210
- }
1211
- if (job.planId && job._isReview) {
1212
- const group = jobGroups.get(job.planId);
1213
- if (group) {
1214
- const review = parseReview(spawnResult.text);
1215
- if (review) {
1216
- group.status = review.verdict === "pass" ? "done" : "executing";
1217
- console.log(`${tag} Review verdict: ${review.verdict} \u2014 ${review.summary}`);
1218
- queue.broadcast(
1219
- { type: "plan_review", planId: job.planId, verdict: review.verdict, summary: review.summary, issues: review.issues },
1220
- job.id
1221
- );
1222
- }
1223
- }
1224
- }
1225
- if (question) {
1226
- 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"})`);
1227
- queue.broadcast(
1228
- { type: "question", jobId: job.id, threadId: (_g2 = job.threadId) != null ? _g2 : job.id, question, annotationIds: job.annotationIds },
1229
- job.id
1230
- );
1231
- }
1232
- queue.broadcast(
1233
- { type: "done", jobId: job.id, success: true, resolutions: resolutions.length > 0 ? resolutions : void 0, responseText: spawnResult.text, threadId: job.threadId },
1234
- job.id
1235
- );
1236
- } else {
1237
- console.error(`${tag} Error: ${spawnResult.error}`);
1238
- job.status = "error";
1239
- job.error = spawnResult.error;
1240
- queue.broadcast(
1241
- {
1242
- type: "error",
1243
- jobId: job.id,
1244
- message: spawnResult.error || "Unknown error"
1245
- },
1246
- job.id
1247
- );
1248
- }
1249
- });
1250
- const server = createServer(async (req, res) => {
1251
- setCors(req, res);
1252
- if (req.method === "OPTIONS") {
1253
- res.writeHead(204);
1254
- res.end();
1255
- return;
1256
- }
1257
- const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1258
- const path = url.pathname;
1259
- try {
1260
- if (req.method === "POST" && path === "/send") {
1261
- await handleSend(req, res);
1262
- } else if (req.method === "GET" && path === "/events") {
1263
- handleEvents(req, res);
1264
- } else if (req.method === "GET" && path === "/status") {
1265
- handleStatus(res);
1266
- } else if (req.method === "GET" && path === "/capabilities") {
1267
- sendJson(res, 200, { providers: capabilities });
1268
- } else if (req.method === "POST" && path === "/reply") {
1269
- await handleReply(req, res);
1270
- } else if (req.method === "POST" && path === "/cancel") {
1271
- handleCancel(req, res);
1272
- } else if (req.method === "POST" && path === "/plan") {
1273
- await handlePlan(req, res);
1274
- } else if (req.method === "POST" && path === "/plan/approve") {
1275
- await handlePlanApprove(req, res);
1276
- } else if (req.method === "POST" && path === "/plan/execute") {
1277
- await handlePlanExecute(req, res);
1278
- } else if (req.method === "POST" && path === "/plan/review") {
1279
- await handlePlanReview(req, res);
1280
- } else if (req.method === "GET" && path.startsWith("/plan/")) {
1281
- handleGetPlan(path.slice("/plan/".length), res);
1282
- } else if (req.method === "GET" && path.startsWith("/thread/")) {
1283
- const threadId = path.slice("/thread/".length);
1284
- await handleGetThread(threadId, res);
1285
- } else {
1286
- sendJson(res, 404, { error: "Not found" });
1287
- }
1288
- } catch (err) {
1289
- console.error("[Bridge] Request error:", err);
1290
- sendJson(res, 500, {
1291
- error: err instanceof Error ? err.message : "Internal error"
1292
- });
1293
- }
1294
- });
1295
- async function handleSend(req, res) {
1296
- const { screenshot, feedback: feedbackStr, color, provider: providerStr, model: modelStr, pastedImages } = await parseMultipart(req);
1297
- let feedback;
1298
- try {
1299
- feedback = JSON.parse(feedbackStr);
1300
- } catch (e) {
1301
- sendJson(res, 400, { error: "Invalid feedback JSON" });
1302
- return;
1303
- }
1304
- const jobId = randomUUID().slice(0, 8);
1305
- const screenshotPath = join2(tempDir, `screenshot-${jobId}.png`);
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
- }
1316
- const linkedSelectors = feedback.annotations.map((a) => a.linkedSelector).filter((s) => !!s);
1317
- let threadId;
1318
- if (linkedSelectors.length > 0) {
1319
- const existingThread = await threadStore.findContinuationThread(linkedSelectors);
1320
- if (existingThread) {
1321
- threadId = existingThread.id;
1322
- await threadStore.addElementIdentifiers(threadId, linkedSelectors);
1323
- } else {
1324
- const newThread = await threadStore.createThread(jobId, linkedSelectors);
1325
- threadId = newThread.id;
1326
- }
1327
- }
1328
- const annotationIds = feedback.annotations.map((a) => a.id);
1329
- const job = __spreadValues({
1330
- id: jobId,
1331
- status: "queued",
1332
- screenshotPath,
1333
- feedback,
1334
- createdAt: Date.now(),
1335
- color,
1336
- threadId,
1337
- annotationIds,
1338
- provider: providerStr === "claude" || providerStr === "codex" ? providerStr : void 0,
1339
- model: modelStr || void 0
1340
- }, Object.keys(imagePaths).length > 0 ? { imagePaths } : {});
1341
- if (threadId) {
1342
- const feedbackSummary = feedback.annotations.map((a) => a.instruction || `[${a.type}]`).join("; ");
1343
- const feedbackContext = formatFeedbackContext(feedback, Object.keys(imagePaths).length > 0 ? imagePaths : void 0);
1344
- await threadStore.appendMessage(threadId, {
1345
- role: "human",
1346
- timestamp: Date.now(),
1347
- jobId,
1348
- screenshotPath,
1349
- annotationIds,
1350
- feedbackSummary,
1351
- feedbackContext: feedbackContext || void 0
1352
- });
1353
- }
1354
- const position = queue.enqueue(job);
1355
- sendJson(res, 200, { jobId, position, threadId });
1356
- }
1357
- async function handleReply(req, res) {
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);
1375
- }
1376
- } else {
1377
- const chunks = [];
1378
- try {
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];
1385
- } finally {
1386
- try {
1387
- more && (temp = iter.return) && await temp.call(iter);
1388
- } finally {
1389
- if (error)
1390
- throw error[0];
1391
- }
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;
1406
- }
1407
- if (!threadId || !reply) {
1408
- sendJson(res, 400, { error: "Missing threadId or reply" });
1409
- return;
1410
- }
1411
- const thread = await threadStore.getThread(threadId);
1412
- if (!thread) {
1413
- sendJson(res, 404, { error: "Thread not found" });
1414
- return;
1415
- }
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
- }
1423
- let screenshotPath = "";
1424
- {
1425
- const history2 = await threadStore.getThreadHistory(threadId);
1426
- for (let i = history2.length - 1; i >= 0; i--) {
1427
- if (history2[i].screenshotPath) {
1428
- screenshotPath = history2[i].screenshotPath;
1429
- break;
1430
- }
1431
- }
1432
- }
1433
- if (!screenshotPath) {
1434
- sendJson(res, 400, { error: "No screenshot available" });
1435
- return;
1436
- }
1437
- await threadStore.appendMessage(threadId, {
1438
- role: "human",
1439
- timestamp: Date.now(),
1440
- jobId,
1441
- replyToQuestion: reply,
1442
- screenshotPath
1443
- });
1444
- const history = await threadStore.getThreadHistory(threadId);
1445
- const annotationIds = [];
1446
- for (const msg of history) {
1447
- if (msg.annotationIds) {
1448
- for (const id of msg.annotationIds) {
1449
- if (!annotationIds.includes(id)) annotationIds.push(id);
1450
- }
1451
- }
1452
- }
1453
- const replyProvider = providerStr === "claude" || providerStr === "codex" ? providerStr : void 0;
1454
- const prompt = buildReplyPrompt(
1455
- screenshotPath,
1456
- history,
1457
- replyProvider,
1458
- replyImagePaths.length > 0 ? replyImagePaths : void 0
1459
- );
1460
- const job = {
1461
- id: jobId,
1462
- status: "queued",
1463
- screenshotPath,
1464
- feedback: { timestamp: (/* @__PURE__ */ new Date()).toISOString(), url: "", viewport: { width: 0, height: 0 }, scrollPosition: { x: 0, y: 0 }, annotations: [], styleModifications: [] },
1465
- createdAt: Date.now(),
1466
- color,
1467
- threadId,
1468
- annotationIds: annotationIds.length > 0 ? annotationIds : void 0,
1469
- provider: replyProvider,
1470
- model: modelStr || void 0
1471
- };
1472
- job._replyPrompt = prompt;
1473
- const position = queue.enqueue(job);
1474
- sendJson(res, 200, { jobId, position, threadId });
1475
- }
1476
- function handleEvents(req, res) {
1477
- res.writeHead(200, {
1478
- "Content-Type": "text/event-stream",
1479
- "Cache-Control": "no-cache",
1480
- Connection: "keep-alive"
1481
- });
1482
- res.write(`event: connected
20
+ IMPORTANT: First, use the Read tool to view the updated screenshot at: ${o.screenshotPath}`:"");else{let v=!a&&o.threadId?await x.getThreadHistory(o.threadId):void 0;g=a!=null?a:$e(o.screenshotPath,o.feedback,{threadHistory:v&&v.length>0?v:void 0,provider:c,imagePaths:o.imagePaths})}let S=ft(o.color,`[\u22B9 ${e}:${o.id}]`);console.log(`${S} Reviewing feedback ${o.screenshotPath} (provider: ${c})${o.threadId?` (thread: ${o.threadId})`:""}${d?` (resuming: ${d.slice(0,8)})`:""}`);let $=!!o._isPlanExecutor,w="",k=0,F=(v,D)=>{if(h.broadcast(v,D),$&&v.type==="delta"&&"text"in v){w+=v.text;let P=Ae(w);if(P.length>k){let f=P.slice(k);k=P.length,h.broadcast({type:"task_resolved",jobId:D,planId:o.planId,resolutions:f,threadId:o.threadId},D)}}},N=(te=o._allowedTools)!=null?te:i,{process:O,result:E}=c==="codex"?ke(o.id,{prompt:g,projectRoot:s,screenshotPath:o.screenshotPath,resumeSessionId:d,model:o.model,onEvent:F}):Te(o.id,{prompt:g,projectRoot:s,maxTurns:r,maxBudgetUsd:l,allowedTools:N,claudePath:u,resumeSessionId:d,model:o.model,onEvent:F});h.setActiveProcess(o.id,O);let p=await E;if(o.result=p.text,p.success){console.log(`${S} Iteration complete`),p.fileEdits&&p.fileEdits.length>0&&console.log(`${S} Captured ${p.fileEdits.length} file edit(s): ${p.fileEdits.map(f=>`${f.tool} ${f.file_path}`).join(", ")}`),o.status="done";let v=Re(p.text),D=_e(p.text);if(D.length>0&&o.annotationIds&&o.annotationIds.length>0){let f=new Set(o.annotationIds);D.every(ne=>f.has(ne.annotationId))||(D=D.map((ne,Fe)=>ie(ee({},ne),{annotationId:o.annotationIds[Fe%o.annotationIds.length]})))}let P=p.fileEdits&&p.fileEdits.length>0?p.fileEdits.map(f=>`${f.tool} ${f.file_path.split("/").pop()}`):void 0;if(o.threadId&&await x.appendMessage(o.threadId,{role:"assistant",timestamp:Date.now(),jobId:o.id,responseText:p.text,resolutions:D.length>0?D:void 0,question:v!=null?v:void 0,sessionId:p.sessionId,toolsUsed:P}),o.planId&&!o.planTaskId){let f=G.get(o.planId);if(f){let J=Me(p.text);J&&J.length>0?(f.plan=J,f.status="awaiting_approval",f.plannerThreadId=o.threadId,console.log(`${S} Plan ready: ${J.length} tasks for group ${o.planId}`),h.broadcast({type:"plan_ready",jobId:o.id,planId:o.planId,tasks:J,threadId:o.threadId},o.id)):v||(f.status="error",console.error(`${S} Failed to parse plan from planner response`))}}if(o.planId&&o._isReview){let f=G.get(o.planId);if(f){let J=De(p.text);J&&(f.status=J.verdict==="pass"?"done":"executing",console.log(`${S} Review verdict: ${J.verdict} \u2014 ${J.summary}`),h.broadcast({type:"plan_review",planId:o.planId,verdict:J.verdict,summary:J.summary,issues:J.issues},o.id))}}v&&(console.log(`${S} \u{1F4AC} Question detected: "${v.slice(0,120)}" \u2192 broadcasting to ${U.size} SSE clients (threadId=${(b=o.threadId)!=null?b:o.id}, annotationIds=${(se=(j=o.annotationIds)==null?void 0:j.join(","))!=null?se:"none"})`),h.broadcast({type:"question",jobId:o.id,threadId:(fe=o.threadId)!=null?fe:o.id,question:v,annotationIds:o.annotationIds},o.id)),h.broadcast({type:"done",jobId:o.id,success:!0,resolutions:D.length>0?D:void 0,responseText:p.text,threadId:o.threadId},o.id)}else console.error(`${S} Error: ${p.error}`),o.status="error",o.error=p.error,h.broadcast({type:"error",jobId:o.id,message:p.error||"Unknown error"},o.id)});let L=it(async(o,a)=>{if(ht(o,a),o.method==="OPTIONS"){a.writeHead(204),a.end();return}let d=new URL(o.url||"/",`http://127.0.0.1:${e}`).pathname;try{if(o.method==="POST"&&d==="/send")await Y(o,a);else if(o.method==="GET"&&d==="/events")V(o,a);else if(o.method==="GET"&&d==="/status")X(a);else if(o.method==="GET"&&d==="/capabilities")I(a,200,{providers:_});else if(o.method==="POST"&&d==="/reply")await z(o,a);else if(o.method==="POST"&&d==="/cancel")K(o,a);else if(o.method==="POST"&&d==="/plan")await q(o,a);else if(o.method==="POST"&&d==="/plan/approve")await R(o,a);else if(o.method==="POST"&&d==="/plan/execute")await Z(o,a);else if(o.method==="POST"&&d==="/plan/review")await T(o,a);else if(o.method==="GET"&&d.startsWith("/plan/"))A(d.slice(6),a);else if(o.method==="GET"&&d.startsWith("/thread/")){let g=d.slice(8);await y(g,a)}else I(a,404,{error:"Not found"})}catch(g){console.error("[Bridge] Request error:",g),I(a,500,{error:g instanceof Error?g.message:"Internal error"})}});async function Y(o,a){let{screenshot:c,feedback:d,color:g,provider:S,model:$,pastedImages:w}=await ae(o),k;try{k=JSON.parse(d)}catch(b){I(a,400,{error:"Invalid feedback JSON"});return}let F=re().slice(0,8),N=oe(t,`screenshot-${F}.png`);await le(N,c);let O={};if(w.length>0)for(let b of w){let j=oe(t,`pasted-${F}-${b.annotationId}-${b.index}.png`);await le(j,b.data),O[b.annotationId]||(O[b.annotationId]=[]),O[b.annotationId].push(j)}let E=k.annotations.map(b=>b.linkedSelector).filter(b=>!!b),p;if(E.length>0){let b=await x.findContinuationThread(E);b?(p=b.id,await x.addElementIdentifiers(p,E)):p=(await x.createThread(F,E)).id}let M=k.annotations.map(b=>b.id),W=ee({id:F,status:"queued",screenshotPath:N,feedback:k,createdAt:Date.now(),color:g,threadId:p,annotationIds:M,provider:S==="claude"||S==="codex"?S:void 0,model:$||void 0},Object.keys(O).length>0?{imagePaths:O}:{});if(p){let b=k.annotations.map(se=>se.instruction||`[${se.type}]`).join("; "),j=ge(k,Object.keys(O).length>0?O:void 0);await x.appendMessage(p,{role:"human",timestamp:Date.now(),jobId:F,screenshotPath:N,annotationIds:M,feedbackSummary:b,feedbackContext:j||void 0})}let te=h.enqueue(W);I(a,200,{jobId:F,position:te,threadId:p})}async function z(o,a){let c=o.headers["content-type"]||"",d,g,S,$,w,k=[];if(c.includes("multipart/form-data")){let P=await ae(o),f=P.feedback?JSON.parse(P.feedback):{};d=f.threadId,g=f.reply,S=f.color,$=f.provider,w=f.model;for(let J of P.pastedImages)k.push(J.data)}else{let P=[];try{for(var se=ve(o),fe,v,D;fe=!(v=await se.next()).done;fe=!1){let ne=v.value;P.push(typeof ne=="string"?Buffer.from(ne):ne)}}catch(v){D=[v]}finally{try{fe&&(v=se.return)&&await v.call(se)}finally{if(D)throw D[0]}}let f=Buffer.concat(P).toString("utf-8"),J;try{J=JSON.parse(f)}catch(ne){I(a,400,{error:"Invalid JSON"});return}d=J.threadId,g=J.reply,S=J.color,$=J.provider,w=J.model}if(!d||!g){I(a,400,{error:"Missing threadId or reply"});return}if(!await x.getThread(d)){I(a,404,{error:"Thread not found"});return}let N=re().slice(0,8),O=[];for(let P=0;P<k.length;P++){let f=oe(t,`reply-${N}-${P}.png`);await le(f,k[P]),O.push(f)}let E="";{let P=await x.getThreadHistory(d);for(let f=P.length-1;f>=0;f--)if(P[f].screenshotPath){E=P[f].screenshotPath;break}}if(!E){I(a,400,{error:"No screenshot available"});return}await x.appendMessage(d,{role:"human",timestamp:Date.now(),jobId:N,replyToQuestion:g,screenshotPath:E});let p=await x.getThreadHistory(d),M=[];for(let P of p)if(P.annotationIds)for(let f of P.annotationIds)M.includes(f)||M.push(f);let W=$==="claude"||$==="codex"?$:void 0,te=Ce(E,p,W,O.length>0?O:void 0),b={id:N,status:"queued",screenshotPath:E,feedback:{timestamp:new Date().toISOString(),url:"",viewport:{width:0,height:0},scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),color:S,threadId:d,annotationIds:M.length>0?M:void 0,provider:W,model:w||void 0};b._replyPrompt=te;let j=h.enqueue(b);I(a,200,{jobId:N,position:j,threadId:d})}function V(o,a){a.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),a.write(`event: connected
1483
21
  data: {"status":"connected"}
1484
22
 
1485
- `);
1486
- const client = { id: randomUUID().slice(0, 8), res };
1487
- sseClients.add(client);
1488
- req.on("close", () => {
1489
- sseClients.delete(client);
1490
- });
1491
- }
1492
- function handleStatus(res) {
1493
- const allActive = queue.allActive;
1494
- sendJson(res, 200, {
1495
- ok: true,
1496
- activeJob: allActive[0] ? { id: allActive[0].id, status: allActive[0].status } : null,
1497
- activeJobs: allActive.map((j) => ({ id: j.id, status: j.status })),
1498
- queueDepth: queue.depth
1499
- });
1500
- }
1501
- function handleCancel(req, res) {
1502
- const reqUrl = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1503
- const jobId = reqUrl.searchParams.get("jobId");
1504
- const cancelled = jobId ? queue.cancelJob(jobId) : queue.cancelActive();
1505
- sendJson(res, 200, { cancelled });
1506
- }
1507
- async function handlePlan(req, res) {
1508
- const { screenshot, feedback: feedbackStr, goal: goalStr, pageUrl: pageUrlStr, viewport: viewportStr, provider: providerStr, model: modelStr, manifest: manifestStr } = await parseMultipart(req);
1509
- if (!screenshot || !goalStr) {
1510
- sendJson(res, 400, { error: "Missing screenshot or goal" });
1511
- return;
1512
- }
1513
- const pageUrl = pageUrlStr || "";
1514
- let viewport = { width: 1440, height: 900 };
1515
- try {
1516
- if (viewportStr) viewport = JSON.parse(viewportStr);
1517
- } catch (e) {
1518
- }
1519
- let feedbackContext;
1520
- if (feedbackStr) {
1521
- try {
1522
- const feedback = JSON.parse(feedbackStr);
1523
- const ctx = formatFeedbackContext(feedback);
1524
- if (ctx) feedbackContext = ctx;
1525
- } catch (e) {
1526
- }
1527
- }
1528
- const planId = randomUUID().slice(0, 8);
1529
- const jobId = randomUUID().slice(0, 8);
1530
- const screenshotPath = join2(tempDir, `screenshot-plan-${planId}.png`);
1531
- await writeFile2(screenshotPath, screenshot);
1532
- const thread = await threadStore.createThread(jobId, []);
1533
- const threadId = thread.id;
1534
- const group = {
1535
- id: planId,
1536
- goal: goalStr,
1537
- status: "planning",
1538
- plannerJobId: jobId,
1539
- plannerThreadId: threadId,
1540
- workerJobIds: [],
1541
- screenshotPath,
1542
- pageUrl,
1543
- viewport,
1544
- createdAt: Date.now()
1545
- };
1546
- jobGroups.set(planId, group);
1547
- const prompt = buildPlannerPrompt(screenshotPath, goalStr, pageUrl, viewport, manifestStr, feedbackContext);
1548
- await threadStore.appendMessage(threadId, {
1549
- role: "human",
1550
- timestamp: Date.now(),
1551
- jobId,
1552
- screenshotPath,
1553
- feedbackSummary: `Plan: ${goalStr}`,
1554
- feedbackContext: `Goal: ${goalStr}
1555
- Page: ${pageUrl}`
1556
- });
1557
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1558
- const job = {
1559
- id: jobId,
1560
- status: "queued",
1561
- screenshotPath,
1562
- feedback: {
1563
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1564
- url: pageUrl,
1565
- viewport,
1566
- scrollPosition: { x: 0, y: 0 },
1567
- annotations: [],
1568
- styleModifications: []
1569
- },
1570
- createdAt: Date.now(),
1571
- threadId,
1572
- provider,
1573
- model: modelStr || void 0,
1574
- planId
1575
- };
1576
- job._replyPrompt = prompt;
1577
- job._allowedTools = ["Read"];
1578
- const position = queue.enqueue(job);
1579
- sendJson(res, 200, { planId, jobId, position, threadId });
1580
- }
1581
- async function handlePlanApprove(req, res) {
1582
- const chunks = [];
1583
- try {
1584
- for (var iter = __forAwait(req), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
1585
- const chunk = temp.value;
1586
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1587
- }
1588
- } catch (temp) {
1589
- error = [temp];
1590
- } finally {
1591
- try {
1592
- more && (temp = iter.return) && await temp.call(iter);
1593
- } finally {
1594
- if (error)
1595
- throw error[0];
1596
- }
1597
- }
1598
- const body = Buffer.concat(chunks).toString("utf-8");
1599
- let parsed;
1600
- try {
1601
- parsed = JSON.parse(body);
1602
- } catch (e) {
1603
- sendJson(res, 400, { error: "Invalid JSON" });
1604
- return;
1605
- }
1606
- const { planId, approvedTaskIds } = parsed;
1607
- if (!planId) {
1608
- sendJson(res, 400, { error: "Missing planId" });
1609
- return;
1610
- }
1611
- const group = jobGroups.get(planId);
1612
- if (!group) {
1613
- sendJson(res, 404, { error: "Plan not found" });
1614
- return;
1615
- }
1616
- if (!group.plan) {
1617
- sendJson(res, 400, { error: "Plan has no tasks" });
1618
- return;
1619
- }
1620
- const tasks = approvedTaskIds ? group.plan.filter((t) => approvedTaskIds.includes(t.id)) : group.plan;
1621
- group.status = "executing";
1622
- sendJson(res, 200, { planId, tasks, status: "executing" });
1623
- }
1624
- async function handlePlanExecute(req, res) {
1625
- const { screenshot, planId: planIdStr, tasks: tasksStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1626
- if (!planIdStr || !tasksStr || !screenshot) {
1627
- sendJson(res, 400, { error: "Missing planId, tasks, or screenshot" });
1628
- return;
1629
- }
1630
- const group = jobGroups.get(planIdStr);
1631
- if (!group) {
1632
- sendJson(res, 404, { error: "Plan not found" });
1633
- return;
1634
- }
1635
- if (group.status !== "executing") {
1636
- sendJson(res, 400, { error: `Plan status is ${group.status}, expected executing` });
1637
- return;
1638
- }
1639
- let tasks;
1640
- try {
1641
- tasks = JSON.parse(tasksStr);
1642
- } catch (e) {
1643
- sendJson(res, 400, { error: "Invalid tasks JSON" });
1644
- return;
1645
- }
1646
- const jobId = randomUUID().slice(0, 8);
1647
- const screenshotPath = join2(tempDir, `screenshot-exec-${planIdStr}.png`);
1648
- await writeFile2(screenshotPath, screenshot);
1649
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1650
- const prompt = buildPlanExecutorPrompt(screenshotPath, tasks, group.pageUrl, group.viewport, provider);
1651
- const annotationIds = tasks.map((t) => t.annotationId);
1652
- const job = {
1653
- id: jobId,
1654
- status: "queued",
1655
- screenshotPath,
1656
- feedback: {
1657
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1658
- url: group.pageUrl,
1659
- viewport: group.viewport,
1660
- scrollPosition: { x: 0, y: 0 },
1661
- annotations: [],
1662
- styleModifications: []
1663
- },
1664
- createdAt: Date.now(),
1665
- provider,
1666
- model: modelStr || void 0,
1667
- planId: planIdStr,
1668
- annotationIds
1669
- };
1670
- job._replyPrompt = prompt;
1671
- job._isPlanExecutor = true;
1672
- group.executorJobId = jobId;
1673
- const position = queue.enqueue(job);
1674
- sendJson(res, 200, { jobId, planId: planIdStr, position });
1675
- }
1676
- async function handlePlanReview(req, res) {
1677
- const { screenshot, planId: planIdStr, provider: providerStr, model: modelStr } = await parseMultipart(req);
1678
- if (!planIdStr) {
1679
- sendJson(res, 400, { error: "Missing planId" });
1680
- return;
1681
- }
1682
- const group = jobGroups.get(planIdStr);
1683
- if (!group) {
1684
- sendJson(res, 404, { error: "Plan not found" });
1685
- return;
1686
- }
1687
- group.status = "reviewing";
1688
- const jobId = randomUUID().slice(0, 8);
1689
- let screenshotPath = group.screenshotPath;
1690
- if (screenshot) {
1691
- screenshotPath = join2(tempDir, `screenshot-review-${planIdStr}.png`);
1692
- await writeFile2(screenshotPath, screenshot);
1693
- }
1694
- const completedTasks = (group.plan || []).map((t) => ({
1695
- id: t.id,
1696
- instruction: t.instruction,
1697
- summary: "completed"
1698
- // Workers will have set resolution summaries
1699
- }));
1700
- const prompt = buildReviewerPrompt(screenshotPath, group.goal, completedTasks);
1701
- const provider = providerStr === "claude" || providerStr === "codex" ? providerStr : defaultProvider;
1702
- const job = {
1703
- id: jobId,
1704
- status: "queued",
1705
- screenshotPath,
1706
- feedback: {
1707
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1708
- url: group.pageUrl,
1709
- viewport: group.viewport,
1710
- scrollPosition: { x: 0, y: 0 },
1711
- annotations: [],
1712
- styleModifications: []
1713
- },
1714
- createdAt: Date.now(),
1715
- // Don't set threadId — avoids resuming planner session, which would discard the review prompt
1716
- provider,
1717
- model: modelStr || void 0,
1718
- planId: planIdStr
1719
- };
1720
- job._replyPrompt = prompt;
1721
- job._isReview = true;
1722
- job._allowedTools = ["Read"];
1723
- const position = queue.enqueue(job);
1724
- sendJson(res, 200, { jobId, planId: planIdStr, position });
1725
- }
1726
- function handleGetPlan(planId, res) {
1727
- const group = jobGroups.get(planId);
1728
- if (!group) {
1729
- sendJson(res, 404, { error: "Plan not found" });
1730
- return;
1731
- }
1732
- sendJson(res, 200, group);
1733
- }
1734
- async function handleGetThread(threadId, res) {
1735
- const thread = await threadStore.getThread(threadId);
1736
- if (!thread) {
1737
- sendJson(res, 404, { error: "Thread not found" });
1738
- return;
1739
- }
1740
- const messages = thread.messages.map((_a2) => {
1741
- var _b2 = _a2, { screenshotPath } = _b2, rest = __objRest(_b2, ["screenshotPath"]);
1742
- return rest;
1743
- });
1744
- sendJson(res, 200, { id: thread.id, createdAt: thread.createdAt, messages });
1745
- }
1746
- const cleanupTimer = setInterval(() => {
1747
- cleanupTempDir(tempDir).catch(() => {
1748
- });
1749
- }, CLEANUP_INTERVAL_MS);
1750
- return new Promise((resolve, reject) => {
1751
- server.on("error", (err) => {
1752
- if (err.code === "EADDRINUSE") {
1753
- console.log(`[\u22B9 already watching :${port}]`);
1754
- resolve({
1755
- port,
1756
- close: async () => {
1757
- }
1758
- });
1759
- return;
1760
- }
1761
- reject(err);
1762
- });
1763
- server.listen(port, "127.0.0.1", () => {
1764
- console.log(`[\u22B9 is watching :${port}]`);
1765
- resolve({
1766
- port,
1767
- close: async () => {
1768
- clearInterval(cleanupTimer);
1769
- queue.destroy();
1770
- for (const client of sseClients) {
1771
- try {
1772
- client.res.end();
1773
- } catch (e) {
1774
- }
1775
- }
1776
- sseClients.clear();
1777
- return new Promise((res) => {
1778
- server.close(() => res());
1779
- });
1780
- }
1781
- });
1782
- });
1783
- });
1784
- }
1785
- async function cleanupTempDir(tempDir) {
1786
- try {
1787
- const files = await readdir(tempDir);
1788
- const now = Date.now();
1789
- for (const file of files) {
1790
- const filePath = join2(tempDir, file);
1791
- try {
1792
- const stats = await stat(filePath);
1793
- if (now - stats.mtimeMs > MAX_FILE_AGE_MS) {
1794
- await unlink(filePath);
1795
- }
1796
- } catch (e) {
1797
- }
1798
- }
1799
- } catch (e) {
1800
- }
1801
- }
1802
-
1803
- // src/server/index.ts
1804
- async function startPopmelt(options) {
1805
- if (process.env.NODE_ENV === "production" && !(options == null ? void 0 : options.force)) {
1806
- throw new Error(
1807
- "[Bridge] Refusing to start in production. Pass { force: true } to override."
1808
- );
1809
- }
1810
- return createPopmelt(options);
1811
- }
1812
- export {
1813
- startPopmelt
1814
- };
23
+ `);let c={id:re().slice(0,8),res:a};U.add(c),o.on("close",()=>{U.delete(c)})}function X(o){let a=h.allActive;I(o,200,{ok:!0,activeJob:a[0]?{id:a[0].id,status:a[0].status}:null,activeJobs:a.map(c=>({id:c.id,status:c.status})),queueDepth:h.depth})}function K(o,a){let d=new URL(o.url||"/",`http://127.0.0.1:${e}`).searchParams.get("jobId"),g=d?h.cancelJob(d):h.cancelActive();I(a,200,{cancelled:g})}async function q(o,a){let{screenshot:c,feedback:d,goal:g,pageUrl:S,viewport:$,provider:w,model:k,manifest:F}=await ae(o);if(!c||!g){I(a,400,{error:"Missing screenshot or goal"});return}let N=S||"",O={width:1440,height:900};try{$&&(O=JSON.parse($))}catch(P){}let E;if(d)try{let P=JSON.parse(d),f=ge(P);f&&(E=f)}catch(P){}let p=re().slice(0,8),M=re().slice(0,8),W=oe(t,`screenshot-plan-${p}.png`);await le(W,c);let b=(await x.createThread(M,[])).id,j={id:p,goal:g,status:"planning",plannerJobId:M,plannerThreadId:b,workerJobIds:[],screenshotPath:W,pageUrl:N,viewport:O,createdAt:Date.now()};G.set(p,j);let se=Ee(W,g,N,O,F,E);await x.appendMessage(b,{role:"human",timestamp:Date.now(),jobId:M,screenshotPath:W,feedbackSummary:`Plan: ${g}`,feedbackContext:`Goal: ${g}
24
+ Page: ${N}`});let fe=w==="claude"||w==="codex"?w:m,v={id:M,status:"queued",screenshotPath:W,feedback:{timestamp:new Date().toISOString(),url:N,viewport:O,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),threadId:b,provider:fe,model:k||void 0,planId:p};v._replyPrompt=se,v._allowedTools=["Read"];let D=h.enqueue(v);I(a,200,{planId:p,jobId:M,position:D,threadId:b})}async function R(o,a){let c=[];try{for(var F=ve(o),N,O,E;N=!(O=await F.next()).done;N=!1){let p=O.value;c.push(typeof p=="string"?Buffer.from(p):p)}}catch(O){E=[O]}finally{try{N&&(O=F.return)&&await O.call(F)}finally{if(E)throw E[0]}}let d=Buffer.concat(c).toString("utf-8"),g;try{g=JSON.parse(d)}catch(p){I(a,400,{error:"Invalid JSON"});return}let{planId:S,approvedTaskIds:$}=g;if(!S){I(a,400,{error:"Missing planId"});return}let w=G.get(S);if(!w){I(a,404,{error:"Plan not found"});return}if(!w.plan){I(a,400,{error:"Plan has no tasks"});return}let k=$?w.plan.filter(p=>$.includes(p.id)):w.plan;w.status="executing",I(a,200,{planId:S,tasks:k,status:"executing"})}async function Z(o,a){let{screenshot:c,planId:d,tasks:g,provider:S,model:$}=await ae(o);if(!d||!g||!c){I(a,400,{error:"Missing planId, tasks, or screenshot"});return}let w=G.get(d);if(!w){I(a,404,{error:"Plan not found"});return}if(w.status!=="executing"){I(a,400,{error:`Plan status is ${w.status}, expected executing`});return}let k;try{k=JSON.parse(g)}catch(te){I(a,400,{error:"Invalid tasks JSON"});return}let F=re().slice(0,8),N=oe(t,`screenshot-exec-${d}.png`);await le(N,c);let O=S==="claude"||S==="codex"?S:m,E=Ne(N,k,w.pageUrl,w.viewport,O),p=k.map(te=>te.annotationId),M={id:F,status:"queued",screenshotPath:N,feedback:{timestamp:new Date().toISOString(),url:w.pageUrl,viewport:w.viewport,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),provider:O,model:$||void 0,planId:d,annotationIds:p};M._replyPrompt=E,M._isPlanExecutor=!0,w.executorJobId=F;let W=h.enqueue(M);I(a,200,{jobId:F,planId:d,position:W})}async function T(o,a){let{screenshot:c,planId:d,provider:g,model:S}=await ae(o);if(!d){I(a,400,{error:"Missing planId"});return}let $=G.get(d);if(!$){I(a,404,{error:"Plan not found"});return}$.status="reviewing";let w=re().slice(0,8),k=$.screenshotPath;c&&(k=oe(t,`screenshot-review-${d}.png`),await le(k,c));let F=($.plan||[]).map(M=>({id:M.id,instruction:M.instruction,summary:"completed"})),N=Je(k,$.goal,F),O=g==="claude"||g==="codex"?g:m,E={id:w,status:"queued",screenshotPath:k,feedback:{timestamp:new Date().toISOString(),url:$.pageUrl,viewport:$.viewport,scrollPosition:{x:0,y:0},annotations:[],styleModifications:[]},createdAt:Date.now(),provider:O,model:S||void 0,planId:d};E._replyPrompt=N,E._isReview=!0,E._allowedTools=["Read"];let p=h.enqueue(E);I(a,200,{jobId:w,planId:d,position:p})}function A(o,a){let c=G.get(o);if(!c){I(a,404,{error:"Plan not found"});return}I(a,200,c)}async function y(o,a){let c=await x.getThread(o);if(!c){I(a,404,{error:"Thread not found"});return}let d=c.messages.map($=>{var w=$,{screenshotPath:g}=w,S=be(w,["screenshotPath"]);return S});I(a,200,{id:c.id,createdAt:c.createdAt,messages:d})}let H=setInterval(()=>{Ue(t).catch(()=>{})},dt);return new Promise((o,a)=>{L.on("error",c=>{if(c.code==="EADDRINUSE"){console.log(`[\u22B9 already watching :${e}]`),o({port:e,close:async()=>{}});return}a(c)}),L.listen(e,"127.0.0.1",()=>{console.log(`[\u22B9 is watching :${e}]`),o({port:e,close:async()=>{clearInterval(H),h.destroy();for(let c of U)try{c.res.end()}catch(d){}return U.clear(),new Promise(c=>{L.close(()=>c())})}})})})}async function Ue(n){try{let e=await nt(n),s=Date.now();for(let t of e){let r=oe(n,t);try{let l=await ot(r);s-l.mtimeMs>ct&&await rt(r)}catch(l){}}}catch(e){}}async function Wt(n){if(process.env.NODE_ENV==="production"&&!(n!=null&&n.force))throw new Error("[Bridge] Refusing to start in production. Pass { force: true } to override.");return Be(n)}export{Wt as startPopmelt};