loop-canvas 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -24,12 +24,12 @@
24
24
  </div>
25
25
  <div id="hover-box" hidden></div>
26
26
  <div id="empty" hidden>
27
- <div class="empty-card">
28
- <div class="empty-mark"></div>
29
- <h1>An empty canvas</h1>
30
- <p>Ask your agent to fill it:</p>
31
- <code>Use the Loop skill to put this project on a canvas.</code>
32
- <p class="muted">Or press <kbd>N</kbd> to describe a frame and let the agent build it.</p>
27
+ <div class="empty-set">
28
+ <div class="empty-frames">
29
+ <button class="empty-frame" data-name="Desktop" data-w="1440" data-h="1024"><span class="ef-name">Desktop<small>1440 × 1024</small></span><span class="ef-sheet"><b>Describe this page</b></span></button>
30
+ <button class="empty-frame phone" data-name="Mobile" data-w="393" data-h="852"><span class="ef-name">Mobile<small>393 × 852</small></span><span class="ef-sheet"><b>Describe this screen</b></span></button>
31
+ </div>
32
+ <p class="empty-note"><span class="empty-own">Pick one and say what it should be. Or ask your agent: </span><code>Use the Loop skill to put this project on a canvas.</code></p>
33
33
  </div>
34
34
  </div>
35
35
  </div>
package/server.mjs CHANGED
@@ -1,32 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import xt from"node:http";import $ from"node:fs";import _ from"node:fs/promises";import m from"node:path";import se from"node:os";import zt from"node:crypto";import{spawn as Te,execFile as Ht,execFileSync as Ut}from"node:child_process";import Bt from"node:net";import{fileURLToPath as Kt}from"node:url";var Jt=!1,ye=m.dirname(Kt(import.meta.url)),Ne=m.join(ye,"public"),K=m.join(ye,"cli.mjs"),$t=Wn(process.argv.slice(2)),O=m.resolve($t.dir||".loop"),de=m.dirname(O),We=m.join(O,"canvas.json"),vt=m.join(O,"server.json");$.mkdirSync(m.join(O,"frames"),{recursive:!0});var Oe=process.env.LOOP_CLAUDE_BIN||me("claude"),et=process.env.LOOP_CODEX_BIN||me("codex")||["/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex","/Applications/Codex.app/Contents/Resources/codex",m.join(se.homedir(),"Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex")].find(e=>{try{return $.accessSync(e,$.constants.X_OK),!0}catch{return!1}}),fe={claude:{label:"Claude Code",bin:Oe},codex:{label:"Codex",bin:et}},Yt=["fable","opus","sonnet","haiku"],Xt=["low","medium","high","xhigh","max"],dt=0,lt=null;function Zt(){return Date.now()-dt<3e4?lt:(dt=Date.now(),lt=Vt())}function Vt(){let e=m.join(se.homedir(),".codex"),t=[],o={},i=null;try{let r=JSON.parse($.readFileSync(m.join(e,"models_cache.json"),"utf8"));for(let s of Array.isArray(r)?r:r.models||[]){let a=s.slug||s.id;!a||(s.visibility||"list")!=="list"||(t.push(a),o[a]=(s.supported_reasoning_levels||s.supported_reasoning_efforts||[]).map(l=>l?.effort||l).filter(l=>typeof l=="string"))}}catch{}try{i=/^\s*model_reasoning_effort\s*=\s*"([^"]+)"/m.exec($.readFileSync(m.join(e,"config.toml"),"utf8"))?.[1]||null}catch{}return{models:t,efforts:o,effort:i}}function $e(e=be()){let t=d.agent.pick?.[e]||{},o,i,r;if(e==="codex"){let a=Zt(),l=Qt();o={model:l,effort:a.effort},i=a.models,r=a.efforts[t.model||l]||["low","medium","high"]}else{let a=d.agent.chat?.runner==="claude"?d.agent.chat:{};o={model:process.env.LOOP_MODEL||a.model||null,effort:process.env.LOOP_EFFORT||a.effort||null},i=Yt,r=Xt}let s=t.effort||o.effort||null;return{runner:e,models:i,efforts:r,chat:o,pick:{model:t.model||null,effort:t.effort||null},model:t.model||o.model||null,effort:s&&r.includes(s)?s:e==="codex"?null:s}}function Qt(){if(process.env.LOOP_CODEX_MODEL)return process.env.LOOP_CODEX_MODEL;let e=m.join(se.homedir(),".codex"),t=null,o=[];try{t=/^\s*model\s*=\s*"([^"]+)"/m.exec($.readFileSync(m.join(e,"config.toml"),"utf8"))?.[1]||null}catch{}try{let i=JSON.parse($.readFileSync(m.join(e,"models_cache.json"),"utf8"));o=(Array.isArray(i)?i:i.models||[]).filter(r=>(r.visibility||"list")==="list").map(r=>r.slug||r.id).filter(Boolean)}catch{}return t&&(!o.length||o.includes(t))?t:o[0]||t||null}var R=(e=8)=>zt.randomBytes(e).toString("base64url").slice(0,e),d=qt()||{version:1,name:m.basename(de),token:R(16),agent:{mode:"headless"},frames:[],strokes:[],comments:[]},x=new Map,ne=null,le=null;function qt(){try{return JSON.parse($.readFileSync(We,"utf8"))}catch{return null}}function tt(){let e=We+".tmp";$.writeFileSync(e,JSON.stringify(d)),$.renameSync(e,We)}var Ge=null;function en(){clearTimeout(Ge),Ge=setTimeout(tt,120)}d.sections||=[];for(let e of d.frames)delete e.building;for(let e of d.comments)e.status==="working"&&(e.status="error",e.reply="Interrupted \u2014 the server restarted.");for(let e of d.strokes||[]){if(!e.frameId||e.fx!=null)continue;let t=d.frames.find(b=>b.id===e.frameId);if(!t)continue;e.fx=t.x,e.fy=t.y;let o=e.commentId&&d.comments.find(b=>b.id===e.commentId),i=o?.anchor;if(!i||i.scope==="area"||i.scope==="elements"||!Number.isFinite(i.x)||!Number.isFinite(i.y))continue;let r=d.strokes.filter(b=>b.commentId===o.id&&b.frameId===t.id),s=r.flatMap(b=>b.points.filter((j,I)=>I%2===0)),a=r.flatMap(b=>b.points.filter((j,I)=>I%2===1));if(!s.length)continue;let l=t.x+i.x-(Math.min(...s)+Math.max(...s))/2,p=t.y+i.y-(Math.min(...a)+Math.max(...a))/2;if(Math.hypot(l,p)<4)continue;let w=(b,j)=>s.filter((I,A)=>I+b>=t.x&&I+b<=t.x+t.w&&a[A]+j>=t.y&&a[A]+j<=t.y+t.h).length;if(!(w(l,p)<=w(0,0)))for(let b of r)b.fx=t.x-l,b.fy=t.y-p}var tn=(()=>{let e=(o,i)=>{try{return Ut(o,i,{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:1500,cwd:m.dirname(O)}).trim()}catch{return""}},t=process.env.LOOP_NAME||(process.platform==="darwin"?e("id",["-F"]):"")||e("git",["config","user.name"]);if(!t)try{t=se.userInfo().username}catch{}return String(t||"").trim().slice(0,32)||"Host"})();function It(e){let t={};for(let[a,l]of x)t[a]={commentId:l.commentId,step:l.step,startedAt:l.startedAt};let{token:o,cloud:i,...r}=d,s=be();return{...r,hostName:d.hostName||tn,live:t,role:e,version:Ve,update:e==="owner"?V:void 0,emailSharing:!!G,chatListening:Le(),agentBin:s,agentLabel:fe[s].label,models:$e(s),runners:Object.fromEntries(Object.entries(fe).map(([a,l])=>[a,!!l.bin])),share:e==="owner"?Ze():void 0}}var ue="/__loop";function nn(e,t,o){let i=new URL(o),r={...e.headers,host:i.host},s=xt.request({hostname:i.hostname,port:i.port||80,method:e.method,path:e.url,headers:r},a=>{let l={...a.headers};typeof l.location=="string"&&l.location.startsWith(i.origin)&&(l.location=l.location.slice(i.origin.length)||"/"),delete l["x-frame-options"],l["content-security-policy"]&&(l["content-security-policy"]=String(l["content-security-policy"]).replace(/frame-ancestors[^;]*;?/i,"")),t.writeHead(a.statusCode||502,l),a.pipe(t)});s.on("error",a=>{if(t.headersSent)return t.end();t.writeHead(502,{"content-type":"text/html; charset=utf-8"}),t.end(`<body style="font:14px system-ui;color:#8b8880;display:grid;place-items:center;height:100vh;margin:0">The dev server at ${ve(o)} isn't responding (${ve(a.code||a.message)}). Start it, or run <code>loop scan</code>.</body>`)}),e.pipe(s)}var ze=new Set;function He(e,t){e.write(`data: ${JSON.stringify(t)}
2
+ import vt from"node:http";import $ from"node:fs";import P from"node:fs/promises";import m from"node:path";import ae from"node:os";import Bt from"node:crypto";import{spawn as Oe,execFile as Jt,execFileSync as Kt}from"node:child_process";import It from"node:net";import{fileURLToPath as Xt}from"node:url";var Yt=!1,ke=m.dirname(Xt(import.meta.url)),Ge=m.join(ke,"public"),X=m.join(ke,"cli.mjs"),Tt=Un(process.argv.slice(2)),O=m.resolve(Tt.dir||".loop"),le=m.dirname(O),ze=m.join(O,"canvas.json"),Ot=m.join(O,"server.json");$.mkdirSync(m.join(O,"frames"),{recursive:!0});var je=process.env.LOOP_CLAUDE_BIN||me("claude"),tt=process.env.LOOP_CODEX_BIN||me("codex")||["/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex","/Applications/Codex.app/Contents/Resources/codex",m.join(ae.homedir(),"Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex")].find(e=>{try{return $.accessSync(e,$.constants.X_OK),!0}catch{return!1}}),ue={claude:{label:"Claude Code",bin:je},codex:{label:"Codex",bin:tt}},Zt=["fable","opus","sonnet","haiku"],Vt=["low","medium","high","xhigh","max"],lt=0,ft=null;function Qt(){return Date.now()-lt<3e4?ft:(lt=Date.now(),ft=qt())}function qt(){let e=m.join(ae.homedir(),".codex"),t=[],o={},i=null;try{let r=JSON.parse($.readFileSync(m.join(e,"models_cache.json"),"utf8"));for(let s of Array.isArray(r)?r:r.models||[]){let a=s.slug||s.id;!a||(s.visibility||"list")!=="list"||(t.push(a),o[a]=(s.supported_reasoning_levels||s.supported_reasoning_efforts||[]).map(l=>l?.effort||l).filter(l=>typeof l=="string"))}}catch{}try{i=/^\s*model_reasoning_effort\s*=\s*"([^"]+)"/m.exec($.readFileSync(m.join(e,"config.toml"),"utf8"))?.[1]||null}catch{}return{models:t,efforts:o,effort:i}}function ve(e=xe()){let t=d.agent.pick?.[e]||{},o,i,r;if(e==="codex"){let a=Qt(),l=en();o={model:l,effort:a.effort},i=a.models,r=a.efforts[t.model||l]||["low","medium","high"]}else{let a=d.agent.chat?.runner==="claude"?d.agent.chat:{};o={model:process.env.LOOP_MODEL||a.model||null,effort:process.env.LOOP_EFFORT||a.effort||null},i=Zt,r=Vt}let s=t.effort||o.effort||null;return{runner:e,models:i,efforts:r,chat:o,pick:{model:t.model||null,effort:t.effort||null},model:t.model||o.model||null,effort:s&&r.includes(s)?s:e==="codex"?null:s}}function en(){if(process.env.LOOP_CODEX_MODEL)return process.env.LOOP_CODEX_MODEL;let e=m.join(ae.homedir(),".codex"),t=null,o=[];try{t=/^\s*model\s*=\s*"([^"]+)"/m.exec($.readFileSync(m.join(e,"config.toml"),"utf8"))?.[1]||null}catch{}try{let i=JSON.parse($.readFileSync(m.join(e,"models_cache.json"),"utf8"));o=(Array.isArray(i)?i:i.models||[]).filter(r=>(r.visibility||"list")==="list").map(r=>r.slug||r.id).filter(Boolean)}catch{}return t&&(!o.length||o.includes(t))?t:o[0]||t||null}var N=(e=8)=>Bt.randomBytes(e).toString("base64url").slice(0,e),d=tn()||{version:1,name:m.basename(le),token:N(16),agent:{mode:"headless"},frames:[],strokes:[],comments:[]},S=new Map,ne=null,fe=null;function tn(){try{return JSON.parse($.readFileSync(ze,"utf8"))}catch{return null}}function nt(){let e=ze+".tmp";$.writeFileSync(e,JSON.stringify(d)),$.renameSync(e,ze)}var He=null;function nn(){clearTimeout(He),He=setTimeout(nt,120)}d.sections||=[];for(let e of d.frames)delete e.building;for(let e of d.comments)e.status==="working"&&(e.status="error",e.reply="Interrupted \u2014 the server restarted.");for(let e of d.strokes||[]){if(!e.frameId||e.fx!=null)continue;let t=d.frames.find(x=>x.id===e.frameId);if(!t)continue;e.fx=t.x,e.fy=t.y;let o=e.commentId&&d.comments.find(x=>x.id===e.commentId),i=o?.anchor;if(!i||i.scope==="area"||i.scope==="elements"||!Number.isFinite(i.x)||!Number.isFinite(i.y))continue;let r=d.strokes.filter(x=>x.commentId===o.id&&x.frameId===t.id),s=r.flatMap(x=>x.points.filter((j,I)=>I%2===0)),a=r.flatMap(x=>x.points.filter((j,I)=>I%2===1));if(!s.length)continue;let l=t.x+i.x-(Math.min(...s)+Math.max(...s))/2,p=t.y+i.y-(Math.min(...a)+Math.max(...a))/2;if(Math.hypot(l,p)<4)continue;let g=(x,j)=>s.filter((I,_)=>I+x>=t.x&&I+x<=t.x+t.w&&a[_]+j>=t.y&&a[_]+j<=t.y+t.h).length;if(!(g(l,p)<=g(0,0)))for(let x of r)x.fx=t.x-l,x.fy=t.y-p}var on=(()=>{let e=(o,i)=>{try{return Kt(o,i,{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:1500,cwd:m.dirname(O)}).trim()}catch{return""}},t=process.env.LOOP_NAME||(process.platform==="darwin"?e("id",["-F"]):"")||e("git",["config","user.name"]);if(!t)try{t=ae.userInfo().username}catch{}return String(t||"").trim().slice(0,32)||"Host"})();function jt(e){let t={};for(let[a,l]of S)t[a]={commentId:l.commentId,step:l.step,startedAt:l.startedAt};let{token:o,cloud:i,...r}=d,s=xe();return{...r,down:[...ge],hostName:d.hostName||on,live:t,role:e,version:Qe,update:e==="owner"?q:void 0,emailSharing:!!z,chatListening:Re(),agentBin:s,agentLabel:ue[s].label,models:ve(s),runners:Object.fromEntries(Object.entries(ue).map(([a,l])=>[a,!!l.bin])),share:e==="owner"?Ve():void 0}}var re="/__loop";function ut(e,t,o,i=409){e.writeHead(i,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),e.end(`<!doctype html><meta charset="utf-8"><body style="margin:0;height:100vh;display:grid;place-items:center;background:#fff;color:#88888F;font:14px/1.5 Inter,system-ui,sans-serif"><div style="max-width:420px;padding:24px;text-align:center"><div style="color:#17171A;font-weight:600;font-size:15px;margin-bottom:4px">${t}</div>${o}</div></body>`)}var ge=new Set,rn=e=>{try{let t=new URL(e);return/^(localhost|127\.0\.0\.1|\[::1\])$/.test(t.hostname)?t.origin:null}catch{return null}};function sn(){if(!De.size)return;let e=new Set(d.frames.filter(t=>t.kind==="url").map(t=>rn(t.src)).filter(Boolean));for(let t of[...ge])e.has(t)||ge.delete(t);for(let t of e){let o=new URL(t),i=It.connect({host:o.hostname.replace(/^\[|\]$/g,""),port:+o.port||80}),r=!1,s=a=>{r||(r=!0,i.destroy(),a===ge.has(t)&&(a?ge.delete(t):ge.add(t),Z({type:"state"})))};i.setTimeout(1500,()=>s(!1)),i.on("connect",()=>s(!0)),i.on("error",()=>s(!1))}}setInterval(sn,4e3).unref();function an(e,t,o){let i=new URL(o),r={...e.headers,host:i.host},s=vt.request({hostname:i.hostname,port:i.port||80,method:e.method,path:e.url,headers:r},a=>{let l={...a.headers};typeof l.location=="string"&&l.location.startsWith(i.origin)&&(l.location=l.location.slice(i.origin.length)||"/"),delete l["x-frame-options"],l["content-security-policy"]&&(l["content-security-policy"]=String(l["content-security-policy"]).replace(/frame-ancestors[^;]*;?/i,"")),t.writeHead(a.statusCode||502,l),a.pipe(t)});s.on("error",a=>{if(t.headersSent)return t.end();t.writeHead(502,{"content-type":"text/html; charset=utf-8"}),t.end(`<body style="font:14px system-ui;color:#8b8880;display:grid;place-items:center;height:100vh;margin:0">The dev server at ${Ie(o)} isn't responding (${Ie(a.code||a.message)}). Start it, or run <code>loop scan</code>.</body>`)}),e.pipe(s)}var De=new Set;function Ue(e,t){e.write(`data: ${JSON.stringify(t)}
3
3
 
4
- `)}var Tt=["frames","comments","strokes","sections"];function on(e,t){let o={};for(let[i,r]of Object.entries(t))o[i]=Tt.includes(i)&&Array.isArray(r)?new Map(r.map(s=>[s.id,JSON.stringify(s)])):JSON.stringify(r??null);e.last=o}function Ot(e){let t=It(e.role),o=e.last;if(!o)return on(e,t),{type:"state",state:t};let i={type:"patch"},r=!1;for(let[s,a]of Object.entries(t))if(Tt.includes(s)&&Array.isArray(a)){let l=o[s]instanceof Map?o[s]:new Map,p=new Map,w=[];for(let I of a){let A=JSON.stringify(I);p.set(I.id,A),l.get(I.id)!==A&&w.push(I)}let b=[...p.keys()],j=b.length!==l.size||b.join(`
4
+ `)}var At=["frames","comments","strokes","sections"];function cn(e,t){let o={};for(let[i,r]of Object.entries(t))o[i]=At.includes(i)&&Array.isArray(r)?new Map(r.map(s=>[s.id,JSON.stringify(s)])):JSON.stringify(r??null);e.last=o}function _t(e){let t=jt(e.role),o=e.last;if(!o)return cn(e,t),{type:"state",state:t};let i={type:"patch"},r=!1;for(let[s,a]of Object.entries(t))if(At.includes(s)&&Array.isArray(a)){let l=o[s]instanceof Map?o[s]:new Map,p=new Map,g=[];for(let I of a){let _=JSON.stringify(I);p.set(I.id,_),l.get(I.id)!==_&&g.push(I)}let x=[...p.keys()],j=x.length!==l.size||x.join(`
5
5
  `)!==[...l.keys()].join(`
6
- `);(w.length||j)&&((i.lists||={})[s]={up:w,...j?{order:b}:{}},r=!0),o[s]=p}else{let l=JSON.stringify(a??null);o[s]!==l&&((i.set||={})[s]=a??null,o[s]=l,r=!0)}return r?i:null}function Z(e){for(let t of ze){if(e.type!=="state"){He(t.res,e);continue}let o=Ot(t);o&&He(t.res,o)}}var ft=null,G=null,Ue=new Set;function k(e){en(),clearTimeout(ft),ft=setTimeout(()=>Z({type:"state"}),16),e||G?.schedulePush();for(let t of[...Ue])t()}var Ce=new Map;function nt(e){if(e.kind!=="folder"||Ce.has(e.id))return;let t=null;try{let o=$.watch(e.src,{recursive:!0},(i,r)=>{if(!(r&&(r.startsWith(".")||r.includes("node_modules")))){if(clearTimeout(t),Me.add(e.id),x.get(e.id)?.inbox&&r&&B(e.id,`Editing ${m.basename(String(r))}`),e.building&&e.optionOf&&Date.now()-e.createdAt>1500){let s=d.comments.find(a=>a.id===e.optionOf.cid)?.options?.find(a=>a.frameId===e.id);s&&!s.touched&&(s.touched=!0,e.building="filling",k(),x.has(e.optionOf.from)&&B(e.optionOf.from,`Making option ${s.id}${s.placeholder?"":` \xB7 ${s.name}`}`))}t=setTimeout(()=>{Z({type:"frame-changed",frameId:e.id,live:x.has(e.id)}),x.has(e.id)||G?.frameChanged(e)},x.has(e.id)?450:200)}});Ce.set(e.id,o)}catch{}}function _e(e){Ce.get(e)?.close(),Ce.delete(e)}d.frames.forEach(nt);function Be(e=1280,t=800,o){let i=o&&d.frames.find(a=>a.id===o);if(i){let a=(w,b)=>d.frames.some(j=>w<j.x+j.w+60&&w+e+60>j.x&&b<j.y+j.h+60&&b+t+60>j.y),l=i.x+i.w+120,p=i.y;for(let w=0;w<50&&a(l,p);w++)l+=160;return{x:l,y:p}}if(!d.frames.length)return{x:0,y:0};let r=Math.max(...d.frames.map(a=>a.x+a.w)),s=Math.min(...d.frames.map(a=>a.y));return{x:r+120,y:s}}function rn(e){return(e||"frame").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40)||"frame"}function sn(e){let t=e.split(/[?#]/)[0];return t==="/"||!t?"Home":t.split("/").filter(Boolean).map(o=>decodeURIComponent(o).replace(/\.html?$/,"").replace(/[-_]+/g," ").replace(/^\w/,i=>i.toUpperCase())).join(" / ").slice(0,60)}function q({src:e,route:t,name:o,w:i,h:r,x:s,y:a,near:l,dup:p}){if(i=+i||we.Desktop[0],r=+r||we.Desktop[1],t!=null){t="/"+String(t).replace(/^\/+/,"");let M=!p&&d.frames.find(C=>C.kind==="route"&&C.src===t&&C.w===i);if(M)return M;let L=s!=null&&a!=null?{x:+s,y:+a}:Be(i,r,l),E={id:R(6),name:o||t,kind:"route",src:t,entry:"",w:i,h:r,...L,createdAt:Date.now()};return d.frames.push(E),k(),E}let w=/^https?:\/\//.test(e),b=w?"url":"folder",j="index.html";w||(e=m.resolve(de,e),$.existsSync(e)&&$.statSync(e).isFile()&&(j=m.basename(e),e=m.dirname(e)));let I=s!=null&&a!=null?{x:+s,y:+a}:Be(i,r,l),A={id:R(6),name:o||(w?new URL(e).pathname:m.basename(e)),kind:b,src:e,entry:j,w:i,h:r,...I,createdAt:Date.now()};return d.frames.push(A),nt(A),k(),A}var ve=e=>String(e).replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t]),an=e=>`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
7
- <title>${ve(e)}</title><style>html,body{height:100%;margin:0}body{display:grid;place-items:center;font:14px/1.4 ui-sans-serif,system-ui;color:#9a978f;background:#fbfaf7}</style></head>
8
- <body><div>${ve(e)} \u2014 waiting for the agent\u2026</div></body></html>`,cn=e=>`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
9
- <meta name="loop-empty" content="1"><title>${ve(e)}</title><style>html,body{height:100%;margin:0;background:#fff}</style></head>
10
- <body></body></html>`;function ge(e){let t=rn(e),o=m.join(O,"frames",t),i=2;for(;$.existsSync(o);)o=m.join(O,"frames",`${t}-${i++}`);return o}function he(e){let t=new Set(d.frames.map(o=>o.name));if(!t.has(e))return e;for(let o=2;;o++)if(!t.has(`${e} ${o}`))return`${e} ${o}`}function ut(e,t=!1){let o=ge(e);return $.mkdirSync(o,{recursive:!0}),$.writeFileSync(m.join(o,"index.html"),t?cn(e):an(e)),o}var jt=new Set(["node_modules",".git",".loop",".next","dist","build"]);async function re(e,t,o={bytes:3e7}){await _.mkdir(t,{recursive:!0});for(let i of await _.readdir(e,{withFileTypes:!0})){if(i.name.startsWith(".")||jt.has(i.name))continue;let r=m.join(e,i.name),s=m.join(t,i.name);if(i.isDirectory())await re(r,s,o);else if(i.isFile()){let{size:a}=await _.stat(r);if((o.bytes-=a)<0)throw new Error("frame too large to snapshot");await _.copyFile(r,s)}}}async function De(e,t=e,o=new Set){for(let i of await _.readdir(e,{withFileTypes:!0})){if(i.name.startsWith(".")||jt.has(i.name))continue;let r=m.join(e,i.name);i.isDirectory()?await De(r,t,o):o.add(m.relative(t,r))}return o}function ot(e,t,o){for(let i of $.readdirSync(e,{withFileTypes:!0})){if(i.name.startsWith("."))continue;let r=m.join(e,i.name);i.isDirectory()?ot(r,t,o):o.add(m.relative(t,r))}}var ht=new Map;async function it(e,t){if(e.kind!=="folder")return null;let o=m.join(O,"history",t);try{return await re(e.src,o),o}catch{return null}}async function je(e,t){if(e.kind!=="folder")return null;let o=m.join(O,"history",t);try{return await _.rm(o,{recursive:!0,force:!0}),await re(e.src,o),o}catch{return null}}async function Ae(e,t){let o=await De(e.src),i=await De(t);for(let r of o)i.has(r)||await _.rm(m.join(e.src,r),{force:!0});await re(t,e.src)}function mt(e,t={}){let o=t.file_path||t.path||t.notebook_path,i=o?m.basename(o):"",r=String(t.command||"");switch(e){case"Read":return o&&/\.(png|jpe?g|webp|gif)$/i.test(o)?o.includes(`${m.sep}shots${m.sep}`)?"Looking at the screen":o.includes(`${m.sep}annotations${m.sep}`)?"Looking at your marks":"Looking at your reference":i?`Reading ${i}`:"Reading the code";case"Edit":case"MultiEdit":return i?`Editing ${i}`:"Editing";case"Write":return i?`Writing ${i}`:"Writing";case"Glob":case"Grep":return"Searching the code";case"Bash":return/cli\.mjs"? shot\b|\bloop shot\b/.test(r)?"Taking a screenshot":/cli\.mjs"? (new|add-route)\b/.test(r)?"Adding a frame":/cli\.mjs"? option\b/.test(r)?"Setting up an option":r?"Running a command":"Working\u2026";case"TodoWrite":return"Planning";case"WebFetch":case"WebSearch":return"Looking something up";default:return e}}var we={Mobile:[393,852],Tablet:[834,1194],Desktop:[1440,1024]},rt=e=>e.w<=480?"Mobile":e.w>=1100?"Desktop":e.h>=e.w*1.15?"Tablet":"Custom-size";function Je(e){let t=d.frames.filter(o=>o.id!==e?.id);return["","How Loop canvases work (follow these conventions):",`- A frame is one screen at one device size. Standard sizes: Mobile ${we.Mobile.join("\xD7")}, Tablet ${we.Tablet.join("\xD7")}, Desktop ${we.Desktop.join("\xD7")}. When someone asks for a "mobile", "phone", "tablet", "iPad", "desktop" or "web" version, use the matching size.`,"- Change only the project (or this frame's folder). Never edit Loop itself: its canvas, toolbar, menus or .loop/ files are not yours to change.","- A frame IS the device screen: design edge to edge at that size. Never draw a phone or laptop mockup, notch, status bar, home indicator or bezel inside a frame, and never letterbox a small layout in the middle of a big frame.",'- "A mobile version of this" means the same screen, content and copy, re-laid-out for that width: single column, stacked sections, a mobile navigation pattern (top bar, tab bar or menu), tap targets of at least 44px, readable type. Keep the same colours, fonts and components.','- Name frames "<Screen> \u2014 <Device>", e.g. "Settings \u2014 Mobile". New frames go next to the frame they came from.',"- Keep frames consistent with each other: reuse the visual style already on the canvas.",...t.length?["- Other frames on this canvas: "+t.map(o=>`"${o.name}" (${rt(o)} ${o.w}\xD7${o.h}${o.kind==="route"?`, app route ${o.src}`:""})`).join("; ")+"."]:[]]}function st(e,t){let o=[];if(t.kind==="create")return o.push(`On my Loop canvas, design a new screen called "${e.name}" (${rt(e)}, ${e.w}\xD7${e.h}):`,"",t.text,"","Write it as a complete index.html in the current working directory (self-contained HTML/CSS/JS; CDN imports and web fonts are fine, no build step), real content and working interactions where they make sense.","","Design it the way you would if I had asked you directly in a chat: make real design decisions (layout, hierarchy, type, colour, detail) so it looks finished, not like a wireframe.",`Look at it as you go: \`node ${K} shot ${e.id} --dir ${O}\` saves a screenshot of the frame and prints its path. Check it after your first pass and iterate until it looks right at ${e.w}\xD7${e.h}.`,...Je(e),"","When you're done, tell me briefly what you made."),o.join(`
11
- `);if(e.kind==="canvas"){o.push("You are the design agent behind Loop, a canvas of live frames for the project in the current working directory. The user left a general comment on the canvas itself (not on a single frame).","","Frames on the canvas:",...d.frames.map(r=>`- "${r.name}": ${r.kind==="folder"?`static files in ${r.src}`:r.kind==="frozen"?`a frozen snapshot of ${r.from||"an app screen"} (read-only)`:r.kind==="route"?`the app's ${r.src} route (dev server ${d.proxy})`:r.src}`));let i=t.anchor?.near;return t.anchor?.area&&i?.length?o.push("",`They drew a box on the canvas over: ${i.map(r=>`"${r}"`).join(", ")}. Those frames are the focus; the box is approximate.`):i?.length&&o.push("",`The comment was placed near: ${i.map(r=>`"${r}"`).join(", ")}.`),o.push(...Je(null)),o.push("","Comment:",'"""',t.text,'"""',"",`To add a frame: \`node ${K} new "<Screen> \u2014 <Device>" --w <w> --h <h> --dir ${O}\` prints a folder; write a self-contained index.html there.`,At.slice(2),"Decide which frames or source files it concerns and make the change. If it is a question or a note that needs no change, just answer it. When done, reply to the user the way you would in a chat: a short, plain summary of what you changed (one to three sentences). If they asked a question, answer it properly. If something was ambiguous, say what you assumed."),o.join(`
12
- `)}return mn(_t(e,t))}var dn=(e,t)=>{let o=`\`node ${K} shot ${e.id} --dir ${O}\``,i=fn(t)||Ie.has(t.id)&&t.shotSoon;return["Work on this the way you would if I had asked you directly in a chat:",i?`- This is the screen as I see it right now: ${i} (look at it first, along with the relevant code). No need to take that screenshot yourself.`:`- Understand it first: read the relevant code, and look at the screen as I see it (${o} saves a screenshot and prints its path).`,`- Match the effort to what I asked. A small, specific fix (wording, a colour, spacing, one element): just make it, precisely, and don't spend time on more screenshots. Design work, a layout change, or anything vague ("make it nicer", "this feels off"): make real design decisions (hierarchy, spacing, type, colour, detail), then check the result with a screenshot (`+o+") and iterate until it looks right.","- Keep it consistent with the rest of the screen and canvas unless I'm asking you to change the style.","- When you're done, reply like you would in a chat: what you changed, briefly, and anything I should know. Answer questions properly.",At]},At="- If a command or tool is refused, don't look for another way to do the same thing. Finish what you can and say in your reply what you needed and why: I'll see it with an Allow button.",ae=new Map,Ie=new Map;function ln(e,t){if(!Fe||!X||t.kind==="canvas"||t.id===ke)return;let o=ae.get(t.id)||0;e.shotSoon=m.join(O,"shots",`${t.id}-${e.id}.png`);let i=ce(t,void 0,m.basename(e.shotSoon)).then(r=>{e.shot=r,e.shotRev=o}).catch(()=>{e.shotSoon=null}).finally(()=>Ie.delete(e.id));Ie.set(e.id,i)}var fn=e=>e.shot&&e.shotRev===(ae.get(e.frameId)||0)&&$.existsSync(e.shot)?e.shot:null,un=e=>Ie.has(e.id)?Promise.race([Ie.get(e.id),new Promise(t=>setTimeout(t,3e3))]):null;function _t(e,t){let o=[],i=t.parentId?d.comments.filter(l=>(l.id===t.parentId||l.parentId===t.parentId)&&l.id!==t.id&&l.createdAt<t.createdAt):[];if(o.push(`On my Loop canvas I'm looking at "${e.name}", a live ${rt(e)} screen (${e.w}\xD7${e.h}).`),e.kind==="folder"?o.push(`It's a static page: its files are in the current working directory (entry: ${e.entry}).`):e.kind==="frozen"?o.push(`It's a frozen snapshot of ${e.from?`the app's "${e.from}" screen`:"an app screen"}, kept as the "before". Leave the snapshot alone and change the app's source for that screen; its live frames show the result.`):e.kind==="route"?o.push(`It's the app's "${e.src}" route, running live from the project in the current working directory (dev server ${d.proxy}, hot reload; don't restart it).`):o.push(`It's a live page at ${e.src}, served from the project in the current working directory.`),e.kind==="folder")try{$.readFileSync(m.join(e.src,e.entry),"utf8").includes('name="loop-empty"')&&o.push(`It's empty right now: design the whole screen from scratch at exactly ${e.w}\xD7${e.h} in index.html, and remove the loop-empty meta tag.`)}catch{}if(i.length){o.push("","What we said so far about this:");for(let l of i)o.push(l.kind==="note"?`- You: ${l.text}`:`- Me: ${l.text}${l.reply&&l.status!=="interrupted"?`
6
+ `);(g.length||j)&&((i.lists||={})[s]={up:g,...j?{order:x}:{}},r=!0),o[s]=p}else{let l=JSON.stringify(a??null);o[s]!==l&&((i.set||={})[s]=a??null,o[s]=l,r=!0)}return r?i:null}function Z(e){for(let t of De){if(e.type!=="state"){Ue(t.res,e);continue}let o=_t(t);o&&Ue(t.res,o)}}var ht=null,z=null,Be=new Set;function k(e){nn(),clearTimeout(ht),ht=setTimeout(()=>Z({type:"state"}),16),e||z?.schedulePush();for(let t of[...Be])t()}var Me=new Map;function ot(e){if(e.kind!=="folder"||Me.has(e.id))return;let t=null;try{let o=$.watch(e.src,{recursive:!0},(i,r)=>{if(!(r&&(r.startsWith(".")||r.includes("node_modules")))){if(clearTimeout(t),Fe.add(e.id),S.get(e.id)?.inbox&&r&&J(e.id,`Editing ${m.basename(String(r))}`),e.building&&e.optionOf&&Date.now()-e.createdAt>1500){let s=d.comments.find(a=>a.id===e.optionOf.cid)?.options?.find(a=>a.frameId===e.id);s&&!s.touched&&(s.touched=!0,e.building="filling",k(),S.has(e.optionOf.from)&&J(e.optionOf.from,`Making option ${s.id}${s.placeholder?"":` \xB7 ${s.name}`}`))}t=setTimeout(()=>{Z({type:"frame-changed",frameId:e.id,live:S.has(e.id)}),S.has(e.id)||z?.frameChanged(e)},S.has(e.id)?450:200)}});Me.set(e.id,o)}catch{}}function Pe(e){Me.get(e)?.close(),Me.delete(e)}d.frames.forEach(ot);function Je(e=1280,t=800,o){let i=o&&d.frames.find(a=>a.id===o);if(i){let a=(g,x)=>d.frames.some(j=>g<j.x+j.w+60&&g+e+60>j.x&&x<j.y+j.h+60&&x+t+60>j.y),l=i.x+i.w+120,p=i.y;for(let g=0;g<50&&a(l,p);g++)l+=160;return{x:l,y:p}}if(!d.frames.length)return{x:0,y:0};let r=Math.max(...d.frames.map(a=>a.x+a.w)),s=Math.min(...d.frames.map(a=>a.y));return{x:r+120,y:s}}function dn(e){return(e||"frame").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40)||"frame"}function ln(e){let t=e.split(/[?#]/)[0];return t==="/"||!t?"Home":t.split("/").filter(Boolean).map(o=>decodeURIComponent(o).replace(/\.html?$/,"").replace(/[-_]+/g," ").replace(/^\w/,i=>i.toUpperCase())).join(" / ").slice(0,60)}function Q({src:e,route:t,name:o,w:i,h:r,x:s,y:a,near:l,dup:p}){if(i=+i||ye.Desktop[0],r=+r||ye.Desktop[1],t!=null){t="/"+String(t).replace(/^\/+/,"");let A=!p&&d.frames.find(D=>D.kind==="route"&&D.src===t&&D.w===i);if(A)return A;let F=s!=null&&a!=null?{x:+s,y:+a}:Je(i,r,l),C={id:N(6),name:o||t,kind:"route",src:t,entry:"",w:i,h:r,...F,createdAt:Date.now()};return d.frames.push(C),k(),C}let g=/^https?:\/\//.test(e);if(g){let A=null;try{A=new URL(e)}catch{}if(A&&/^(localhost|127\.0\.0\.1|\[::1\])$/.test(A.hostname)&&+A.port===K){if(A.pathname===re||A.pathname.startsWith(re+"/")||!d.proxy)throw new Error(`${e} is the Loop canvas itself, not your app. Give the address your app runs on (loop scan --url http://localhost:<port>), or a folder with an index.html.`);return Q({route:A.pathname+A.search,name:o,w:i,h:r,x:s,y:a,near:l,dup:p})}}let x=g?"url":"folder",j="index.html";g||(e=m.resolve(le,e),$.existsSync(e)&&$.statSync(e).isFile()&&(j=m.basename(e),e=m.dirname(e)));let I=s!=null&&a!=null?{x:+s,y:+a}:Je(i,r,l),_={id:N(6),name:o||(g?new URL(e).pathname:m.basename(e)),kind:x,src:e,entry:j,w:i,h:r,...I,createdAt:Date.now()};return d.frames.push(_),ot(_),k(),_}var Ie=e=>String(e).replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t]),fn=e=>`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <title>${Ie(e)}</title><style>html,body{height:100%;margin:0}body{display:grid;place-items:center;font:14px/1.4 ui-sans-serif,system-ui;color:#9a978f;background:#fbfaf7}</style></head>
8
+ <body><div>${Ie(e)} \u2014 waiting for the agent\u2026</div></body></html>`,un=e=>`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
9
+ <meta name="loop-empty" content="1"><title>${Ie(e)}</title><style>html,body{height:100%;margin:0;background:#fff}</style></head>
10
+ <body></body></html>`;function we(e){let t=dn(e),o=m.join(O,"frames",t),i=2;for(;$.existsSync(o);)o=m.join(O,"frames",`${t}-${i++}`);return o}function he(e){let t=new Set(d.frames.map(o=>o.name));if(!t.has(e))return e;for(let o=2;;o++)if(!t.has(`${e} ${o}`))return`${e} ${o}`}function mt(e,t=!1){let o=we(e);return $.mkdirSync(o,{recursive:!0}),$.writeFileSync(m.join(o,"index.html"),t?un(e):fn(e)),o}var Pt=new Set(["node_modules",".git",".loop",".next","dist","build"]);async function se(e,t,o={bytes:3e7}){await P.mkdir(t,{recursive:!0});for(let i of await P.readdir(e,{withFileTypes:!0})){if(i.name.startsWith(".")||Pt.has(i.name))continue;let r=m.join(e,i.name),s=m.join(t,i.name);if(i.isDirectory())await se(r,s,o);else if(i.isFile()){let{size:a}=await P.stat(r);if((o.bytes-=a)<0)throw new Error("frame too large to snapshot");await P.copyFile(r,s)}}}async function Le(e,t=e,o=new Set){for(let i of await P.readdir(e,{withFileTypes:!0})){if(i.name.startsWith(".")||Pt.has(i.name))continue;let r=m.join(e,i.name);i.isDirectory()?await Le(r,t,o):o.add(m.relative(t,r))}return o}function it(e,t,o){for(let i of $.readdirSync(e,{withFileTypes:!0})){if(i.name.startsWith("."))continue;let r=m.join(e,i.name);i.isDirectory()?it(r,t,o):o.add(m.relative(t,r))}}var pt=new Map;async function rt(e,t){if(e.kind!=="folder")return null;let o=m.join(O,"history",t);try{return await se(e.src,o),o}catch{return null}}async function Ae(e,t){if(e.kind!=="folder")return null;let o=m.join(O,"history",t);try{return await P.rm(o,{recursive:!0,force:!0}),await se(e.src,o),o}catch{return null}}async function _e(e,t){let o=await Le(e.src),i=await Le(t);for(let r of o)i.has(r)||await P.rm(m.join(e.src,r),{force:!0});await se(t,e.src)}function gt(e,t={}){let o=t.file_path||t.path||t.notebook_path,i=o?m.basename(o):"",r=String(t.command||"");switch(e){case"Read":return o&&/\.(png|jpe?g|webp|gif)$/i.test(o)?o.includes(`${m.sep}shots${m.sep}`)?"Looking at the screen":o.includes(`${m.sep}annotations${m.sep}`)?"Looking at your marks":"Looking at your reference":i?`Reading ${i}`:"Reading the code";case"Edit":case"MultiEdit":return i?`Editing ${i}`:"Editing";case"Write":return i?`Writing ${i}`:"Writing";case"Glob":case"Grep":return"Searching the code";case"Bash":return/cli\.mjs"? shot\b|\bloop shot\b/.test(r)?"Taking a screenshot":/cli\.mjs"? (new|add-route)\b/.test(r)?"Adding a frame":/cli\.mjs"? option\b/.test(r)?"Setting up an option":r?"Running a command":"Working\u2026";case"TodoWrite":return"Planning";case"WebFetch":case"WebSearch":return"Looking something up";default:return e}}var ye={Mobile:[393,852],Tablet:[834,1194],Desktop:[1440,1024]},st=e=>e.w<=480?"Mobile":e.w>=1100?"Desktop":e.h>=e.w*1.15?"Tablet":"Custom-size";function Ke(e){let t=d.frames.filter(o=>o.id!==e?.id);return["","How Loop canvases work (follow these conventions):",`- A frame is one screen at one device size. Standard sizes: Mobile ${ye.Mobile.join("\xD7")}, Tablet ${ye.Tablet.join("\xD7")}, Desktop ${ye.Desktop.join("\xD7")}. When someone asks for a "mobile", "phone", "tablet", "iPad", "desktop" or "web" version, use the matching size.`,"- Change only the project (or this frame's folder). Never edit Loop itself: its canvas, toolbar, menus or .loop/ files are not yours to change.","- A frame IS the device screen: design edge to edge at that size. Never draw a phone or laptop mockup, notch, status bar, home indicator or bezel inside a frame, and never letterbox a small layout in the middle of a big frame.",'- "A mobile version of this" means the same screen, content and copy, re-laid-out for that width: single column, stacked sections, a mobile navigation pattern (top bar, tab bar or menu), tap targets of at least 44px, readable type. Keep the same colours, fonts and components.','- Name frames "<Screen> \u2014 <Device>", e.g. "Settings \u2014 Mobile". New frames go next to the frame they came from.',"- Keep frames consistent with each other: reuse the visual style already on the canvas.",...t.length?["- Other frames on this canvas: "+t.map(o=>`"${o.name}" (${st(o)} ${o.w}\xD7${o.h}${o.kind==="route"?`, app route ${o.src}`:""})`).join("; ")+"."]:[]]}function at(e,t){let o=[];if(t.kind==="create")return o.push(`On my Loop canvas, design a new screen called "${e.name}" (${st(e)}, ${e.w}\xD7${e.h}):`,"",t.text,"","Write it as a complete index.html in the current working directory (self-contained HTML/CSS/JS; CDN imports and web fonts are fine, no build step), real content and working interactions where they make sense.","","Design it the way you would if I had asked you directly in a chat: make real design decisions (layout, hierarchy, type, colour, detail) so it looks finished, not like a wireframe.",`Look at it as you go: \`node ${X} shot ${e.id} --dir ${O}\` saves a screenshot of the frame and prints its path. Check it after your first pass and iterate until it looks right at ${e.w}\xD7${e.h}.`,...Ke(e),"","When you're done, tell me briefly what you made."),o.join(`
11
+ `);if(e.kind==="canvas"){o.push("You are the design agent behind Loop, a canvas of live frames for the project in the current working directory. The user left a general comment on the canvas itself (not on a single frame).","","Frames on the canvas:",...d.frames.map(r=>`- "${r.name}": ${r.kind==="folder"?`static files in ${r.src}`:r.kind==="frozen"?`a frozen snapshot of ${r.from||"an app screen"} (read-only)`:r.kind==="route"?`the app's ${r.src} route (dev server ${d.proxy})`:r.src}`));let i=t.anchor?.near;return t.anchor?.area&&i?.length?o.push("",`They drew a box on the canvas over: ${i.map(r=>`"${r}"`).join(", ")}. Those frames are the focus; the box is approximate.`):i?.length&&o.push("",`The comment was placed near: ${i.map(r=>`"${r}"`).join(", ")}.`),o.push(...Ke(null)),o.push("","Comment:",'"""',t.text,'"""',"",`To add a frame: \`node ${X} new "<Screen> \u2014 <Device>" --w <w> --h <h> --dir ${O}\` prints a folder; write a self-contained index.html there.`,Et.slice(2),"Decide which frames or source files it concerns and make the change. If it is a question or a note that needs no change, just answer it. When done, reply to the user the way you would in a chat: a short, plain summary of what you changed (one to three sentences). If they asked a question, answer it properly. If something was ambiguous, say what you assumed."),o.join(`
12
+ `)}return yn(Ct(e,t))}var hn=(e,t)=>{let o=`\`node ${X} shot ${e.id} --dir ${O}\``,i=pn(t)||Te.has(t.id)&&t.shotSoon;return["Work on this the way you would if I had asked you directly in a chat:",i?`- This is the screen as I see it right now: ${i} (look at it first, along with the relevant code). No need to take that screenshot yourself.`:`- Understand it first: read the relevant code, and look at the screen as I see it (${o} saves a screenshot and prints its path).`,`- Match the effort to what I asked. A small, specific fix (wording, a colour, spacing, one element): just make it, precisely, and don't spend time on more screenshots. Design work, a layout change, or anything vague ("make it nicer", "this feels off"): make real design decisions (hierarchy, spacing, type, colour, detail), then check the result with a screenshot (`+o+") and iterate until it looks right.","- Keep it consistent with the rest of the screen and canvas unless I'm asking you to change the style.","- When you're done, reply like you would in a chat: what you changed, briefly, and anything I should know. Answer questions properly.",Et]},Et="- If a command or tool is refused, don't look for another way to do the same thing. Finish what you can and say in your reply what you needed and why: I'll see it with an Allow button.",ce=new Map,Te=new Map;function mn(e,t){if(!Ne||!K||t.kind==="canvas"||t.id===be)return;let o=ce.get(t.id)||0;e.shotSoon=m.join(O,"shots",`${t.id}-${e.id}.png`);let i=de(t,void 0,m.basename(e.shotSoon)).then(r=>{e.shot=r,e.shotRev=o}).catch(()=>{e.shotSoon=null}).finally(()=>Te.delete(e.id));Te.set(e.id,i)}var pn=e=>e.shot&&e.shotRev===(ce.get(e.frameId)||0)&&$.existsSync(e.shot)?e.shot:null,gn=e=>Te.has(e.id)?Promise.race([Te.get(e.id),new Promise(t=>setTimeout(t,3e3))]):null;function Ct(e,t){let o=[],i=t.parentId?d.comments.filter(l=>(l.id===t.parentId||l.parentId===t.parentId)&&l.id!==t.id&&l.createdAt<t.createdAt):[];if(o.push(`On my Loop canvas I'm looking at "${e.name}", a live ${st(e)} screen (${e.w}\xD7${e.h}).`),e.kind==="folder"?o.push(`It's a static page: its files are in the current working directory (entry: ${e.entry}).`):e.kind==="frozen"?o.push(`It's a frozen snapshot of ${e.from?`the app's "${e.from}" screen`:"an app screen"}, kept as the "before". Leave the snapshot alone and change the app's source for that screen; its live frames show the result.`):e.kind==="route"?o.push(`It's the app's "${e.src}" route, running live from the project in the current working directory (dev server ${d.proxy}, hot reload; don't restart it).`):o.push(`It's a live page at ${e.src}, served from the project in the current working directory.`),e.kind==="folder")try{$.readFileSync(m.join(e.src,e.entry),"utf8").includes('name="loop-empty"')&&o.push(`It's empty right now: design the whole screen from scratch at exactly ${e.w}\xD7${e.h} in index.html, and remove the loop-empty meta tag.`)}catch{}if(i.length){o.push("","What we said so far about this:");for(let l of i)o.push(l.kind==="note"?`- You: ${l.text}`:`- Me: ${l.text}${l.reply&&l.status!=="interrupted"?`
13
13
  - You: ${l.reply}`:l.status==="interrupted"?`
14
14
  - (you were working on that when I interrupted)`:""}`)}t.steeredFrom&&o.push("","I interrupted you to add this. The files may already have part of that work in them: carry on from where things are and do what both messages ask (this one wins where they conflict)."),o.push("",t.text);let r=t.anchor,s=[];if(r?.scope==="area")s.push(`I highlighted an area: x=${Math.round(r.x)}, y=${Math.round(r.y)}, ${Math.round(r.w)}\xD7${Math.round(r.h)}px in the ${e.w}\xD7${e.h} viewport. It's roughly where to look, not a strict boundary.`),r.elements?.length&&s.push("Inside it: "+r.elements.map(l=>`<${l.tag}> \`${l.selector}\`${l.text?` ("${l.text}")`:""}`).join("; ")+"."),r.container&&s.push(`Smallest element wrapping it: <${r.container.tag}> \`${r.container.selector}\`.`);else if(r?.scope==="elements"&&r.elements?.length){s.push(`I picked ${r.elements.length===1?"this exact element":`these ${r.elements.length} exact elements`}:`);for(let l of r.elements.slice(0,10))s.push(`- <${l.tag}> \`${l.selector}\`${l.text?` ("${l.text}")`:""}`),l.html&&s.push(" ```html"," "+l.html.replace(/\n/g,`
15
- `)," ```")}else r?.scope==="frame"?s.push("I mean the whole screen."):r&&r.selector?(s.push(`My comment pin is on <${r.tag}> \`${r.selector}\`${r.text?` ("${r.text}")`:""}. It may be about that element or the area around it.`),r.html&&s.push("```html",r.html,"```")):r&&s.push(`I pointed at x=${Math.round(r.x)}, y=${Math.round(r.y)} in the viewport.`);t.ink?.length&&s.push(`I drew marks over: ${t.ink.map(l=>`\`${l.selector}\`${l.text?` ("${l.text}")`:""}`).join(", ")}.`),t.annotation&&s.push(`A picture of my marks over the screen: ${t.annotation} (look at it).`),t.attachments?.length&&s.push(`I attached ${t.attachments.length===1?"a reference":`${t.attachments.length} references`}; look at ${t.attachments.length===1?"it":"them"} first: ${t.attachments.map(l=>`${l.path} (${l.type})`).join(", ")}.`);let a=hn(e,r);return a&&s.push(a),s.length&&o.push("",...s),o.push("",...dn(e,t),"",`(This message's id, for Loop commands: ${t.id})`),{message:o.join(`
16
- `),reference:Pt(e)}}function hn(e,t){if(e.kind!=="folder"||!t)return"";let o;try{o=$.readFileSync(m.join(e.src,e.entry||"index.html"),"utf8")}catch{return""}let i=[...(t.elements||[]).map(s=>s.html),t.html,t.container?.html].filter(Boolean),r=[];for(let s of i){let a=/^<[^>]+>/.exec(s)?.[0];if(!a)continue;let l=o.indexOf(a);l<0||o.indexOf(a,l+1)>=0||r.push(o.slice(0,l).split(`
17
- `).length)}return r.length?`It is in ${e.entry||"index.html"} at line ${[...new Set(r)].sort((s,a)=>s-a).join(", ")}: open it there, no need to search for it.`:""}function Pt(e){let t=[],[o,i]=we.Mobile;t.push("You are working inside Loop, a canvas of live frames the user reviews and comments on. Their comments reach you as messages."),t.push("","New frames: if they ask for a new frame (a mobile/desktop version, or a separate copy to keep side by side), leave this frame as it is and add one next to it:"),e.kind==="route"?t.push(`- The same app screen at another size: \`node ${K} add-route ${e.src} --name "<Screen> \u2014 Mobile" --w ${o} --h ${i} --near ${e.id} --dir ${O}\`. It's the real app, so if it doesn't fit that width, fix the responsive styles in the source.`,`- A brand-new static design: \`node ${K} new "<Screen> \u2014 <Device>" --w <w> --h <h> --near ${e.id} --dir ${O}\` prints a folder; write a complete self-contained index.html there.`):t.push(`- \`node ${K} new "<Screen> \u2014 <Device>" --w <w> --h <h> --near ${e.id} --dir ${O}\` (e.g. --w ${o} --h ${i} for mobile) prints a folder; write a complete, self-contained index.html there, based on this frame and adapted to that size.`);let r=e.kind==="folder"?e.src:de;return e.kind==="folder"?t.push("",`Options: if they ask for options to choose from (options, ideas, alternatives, variations), leave this frame's files alone. Make 2 to 4 genuinely different options (as many as they asked for). Each message ends with its id; for each option run \`node ${K} option <message id> "<short name>" --dir ${O}\`. It prints a folder that is a full copy of this frame and is already on the canvas as its own frame beside this one: make that option's changes there, editing its files in place. They watch each one fill in as you work, so start the first option early and finish one before starting the next. \`node ${K} shot ${e.id} --option <message id>:A --dir ${O}\` shows how option A looks. Reply with a line per option on how it differs.`):t.push("",`Options: if they ask for options to choose from (options, ideas, alternatives, variations), don't change the real files. Make 2 to 4 genuinely different options. Each message ends with its id; for each option, \`node ${K} option <message id> "<short name>" --dir ${O}\` prints an empty folder: write the complete new version of every file that option changes there, at the same path relative to ${r}. They preview the options live in the frame and pick one; reply with a line per option on how it differs.`),e.kind==="folder"&&t.push("","This frame is self-contained: keep its files in its folder (CDN imports are fine, no build step)."),t.push(...Je(e)),t.join(`
18
- `)}function mn(e){return e.reference?`${e.message}
15
+ `)," ```")}else r?.scope==="frame"?s.push("I mean the whole screen."):r&&r.selector?(s.push(`My comment pin is on <${r.tag}> \`${r.selector}\`${r.text?` ("${r.text}")`:""}. It may be about that element or the area around it.`),r.html&&s.push("```html",r.html,"```")):r&&s.push(`I pointed at x=${Math.round(r.x)}, y=${Math.round(r.y)} in the viewport.`);t.ink?.length&&s.push(`I drew marks over: ${t.ink.map(l=>`\`${l.selector}\`${l.text?` ("${l.text}")`:""}`).join(", ")}.`),t.annotation&&s.push(`A picture of my marks over the screen: ${t.annotation} (look at it).`),t.attachments?.length&&s.push(`I attached ${t.attachments.length===1?"a reference":`${t.attachments.length} references`}; look at ${t.attachments.length===1?"it":"them"} first: ${t.attachments.map(l=>`${l.path} (${l.type})`).join(", ")}.`);let a=wn(e,r);return a&&s.push(a),s.length&&o.push("",...s),o.push("",...hn(e,t),"",`(This message's id, for Loop commands: ${t.id})`),{message:o.join(`
16
+ `),reference:Dt(e)}}function wn(e,t){if(e.kind!=="folder"||!t)return"";let o;try{o=$.readFileSync(m.join(e.src,e.entry||"index.html"),"utf8")}catch{return""}let i=[...(t.elements||[]).map(s=>s.html),t.html,t.container?.html].filter(Boolean),r=[];for(let s of i){let a=/^<[^>]+>/.exec(s)?.[0];if(!a)continue;let l=o.indexOf(a);l<0||o.indexOf(a,l+1)>=0||r.push(o.slice(0,l).split(`
17
+ `).length)}return r.length?`It is in ${e.entry||"index.html"} at line ${[...new Set(r)].sort((s,a)=>s-a).join(", ")}: open it there, no need to search for it.`:""}function Dt(e){let t=[],[o,i]=ye.Mobile;t.push("You are working inside Loop, a canvas of live frames the user reviews and comments on. Their comments reach you as messages."),t.push("","New frames: if they ask for a new frame (a mobile/desktop version, or a separate copy to keep side by side), leave this frame as it is and add one next to it:"),e.kind==="route"?t.push(`- The same app screen at another size: \`node ${X} add-route ${e.src} --name "<Screen> \u2014 Mobile" --w ${o} --h ${i} --near ${e.id} --dir ${O}\`. It's the real app, so if it doesn't fit that width, fix the responsive styles in the source.`,`- A brand-new static design: \`node ${X} new "<Screen> \u2014 <Device>" --w <w> --h <h> --near ${e.id} --dir ${O}\` prints a folder; write a complete self-contained index.html there.`):t.push(`- \`node ${X} new "<Screen> \u2014 <Device>" --w <w> --h <h> --near ${e.id} --dir ${O}\` (e.g. --w ${o} --h ${i} for mobile) prints a folder; write a complete, self-contained index.html there, based on this frame and adapted to that size.`);let r=e.kind==="folder"?e.src:le;return e.kind==="folder"?t.push("",`Options: if they ask for options to choose from (options, ideas, alternatives, variations), leave this frame's files alone. Make 2 to 4 genuinely different options (as many as they asked for). Each message ends with its id; for each option run \`node ${X} option <message id> "<short name>" --dir ${O}\`. It prints a folder that is a full copy of this frame and is already on the canvas as its own frame beside this one: make that option's changes there, editing its files in place. They watch each one fill in as you work, so start the first option early and finish one before starting the next. \`node ${X} shot ${e.id} --option <message id>:A --dir ${O}\` shows how option A looks. Reply with a line per option on how it differs.`):t.push("",`Options: if they ask for options to choose from (options, ideas, alternatives, variations), don't change the real files. Make 2 to 4 genuinely different options. Each message ends with its id; for each option, \`node ${X} option <message id> "<short name>" --dir ${O}\` prints an empty folder: write the complete new version of every file that option changes there, at the same path relative to ${r}. They preview the options live in the frame and pick one; reply with a line per option on how it differs.`),e.kind==="folder"&&t.push("","This frame is self-contained: keep its files in its folder (CDN imports are fine, no build step)."),t.push(...Ke(e)),t.join(`
18
+ `)}function yn(e){return e.reference?`${e.message}
19
19
 
20
20
  ---
21
21
  How Loop works (reference):
22
- ${e.reference}`:e.message}var ke="__canvas";function z(e){return e!==ke?d.frames.find(t=>t.id===e):(d.canvasFrame||={id:ke,name:"Canvas",kind:"canvas",src:de,entry:"",w:0,h:0},d.canvasFrame)}var Ke=0,Le=()=>Date.now()-Ke<45e3;function pn(e){let t=x.get(e.frameId),o=t&&d.comments.find(s=>s.id===t.commentId),i=e.parentId||e.id;if(e.status!=="queued"||!o||o.id!==i&&o.parentId!==i)return!1;e.steeredFrom=o.id,d.comments.splice(d.comments.indexOf(e),1);let r=d.comments.findIndex(s=>s.frameId===e.frameId&&s.status==="queued");return d.comments.splice(r<0?d.comments.length:r,0,e),t.steered=!0,t.canceled=!0,t.child?t.child.kill("SIGTERM"):Y(o,"canceled","Stopped."),k(),!0}function Et(e){return e.kind==="folder"?e.src:de}async function pt(e){let t=new Set;try{await De(e,e,t)}catch{}return[...t]}async function gt(e,t){let o=z(e.frameId);if(!o)throw new Error("no frame");if(e.optionChosen)throw new Error("an option was already chosen");let i=Et(o),r=m.join(O,"options",e.id,"_original"),s=new Set;for(let p of e.options||[])for(let w of await pt(p.dir))s.add(w);if(!$.existsSync(r)){await _.mkdir(r,{recursive:!0});let p=[];for(let w of s){let b=m.join(i,w);oe(i,b)&&($.existsSync(b)?(await _.mkdir(m.dirname(m.join(r,w)),{recursive:!0}),await _.copyFile(b,m.join(r,w))):p.push(w))}await _.writeFile(m.join(r,".missing.json"),JSON.stringify(p))}let a=JSON.parse(await _.readFile(m.join(r,".missing.json"),"utf8").catch(()=>"[]"));for(let p of s){let w=m.join(i,p);oe(i,w)&&(a.includes(p)?await _.rm(w,{force:!0}):$.existsSync(m.join(r,p))&&await _.copyFile(m.join(r,p),w))}let l=t&&(e.options||[]).find(p=>p.id===t);if(t&&!l)throw new Error("no such option");if(l)for(let p of await pt(l.dir)){let w=m.join(i,p);oe(i,w)&&(await _.mkdir(m.dirname(w),{recursive:!0}),await _.copyFile(m.join(l.dir,p),w))}e.optionApplied=l?l.id:null,k(),Z({type:"frame-changed",frameId:o.id})}var Fe=process.env.LOOP_CHROME||["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge","/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",m.join(se.homedir(),"Applications/Google Chrome.app/Contents/MacOS/Google Chrome")].find(e=>$.existsSync(e))||me("google-chrome")||me("chromium")||me("chromium-browser");function gn(e){return e.kind==="folder"||e.kind==="frozen"?`http://127.0.0.1:${X}/__loop/f/${e.id}/${encodeURIComponent(e.entry||"index.html")}`:e.kind==="route"?`http://127.0.0.1:${X}${e.src}`:e.src}var W=null;function Ye(){let e=W;if(W=null,!!e){clearTimeout(e.idle);try{e.child.kill("SIGKILL")}catch{}$.rm(e.profile,{recursive:!0,force:!0},()=>{})}}async function Ct(){if(W)return clearTimeout((await W).idle),W;W=(async()=>{let t=$.mkdtempSync(m.join(se.tmpdir(),"loop-shot-")),o=Te(Fe,["--headless=new","--use-angle=swiftshader","--enable-unsafe-swiftshader","--ignore-gpu-blocklist","--hide-scrollbars","--no-first-run","--no-default-browser-check","--mute-audio",`--user-data-dir=${t}`,"--remote-debugging-port=0","--window-size=1280,900","about:blank"],{stdio:"ignore"}),i={child:o,profile:t,port:null,idle:null};o.on("exit",()=>{W&&(W===e||W===i)&&(W=null,$.rm(t,{recursive:!0,force:!0},()=>{}))});for(let r=0;r<100&&!i.port;r++)try{i.port=$.readFileSync(m.join(t,"DevToolsActivePort"),"utf8").split(`
23
- `)[0]}catch{await new Promise(s=>setTimeout(s,50))}if(!i.port){try{o.kill("SIGKILL")}catch{}throw new Error("Chrome did not start")}return i})();let e=W;try{let t=await e;return W===e&&(W=t),t}catch(t){throw W===e&&(W=null),t}}var xe=new Map,Me=new Set;async function wn(e){let t=`${e.w}x${e.h}`,o=xe.get(e.id);if(o&&!(o instanceof Promise)&&o.size===t&&!Me.has(e.id)&&$.existsSync(o.file))return o.file;if(o instanceof Promise)return(await o).file;Me.delete(e.id);let i=ce(e).then(r=>{let s=xe.get(e.id),a={file:r,size:t};return xe.set(e.id,a),a}).catch(r=>{throw xe.delete(e.id),r});return xe.set(e.id,i),(await i).file}var wt=Promise.resolve();function ce(e,t,o){let i=wt.then(()=>yn(e,t,o));return wt=i.catch(()=>{}),i}async function yn(e,t,o){if(!Fe)throw new Error("No Chrome, Edge, Brave or Chromium found for screenshots (set LOOP_CHROME to one).");let i=globalThis.WebSocket;if(!i)throw new Error("Screenshots need Node 22 or newer");let r=m.join(O,"shots");$.mkdirSync(r,{recursive:!0});let s=m.join(r,o||`${e.id}-${Date.now()}${R(3)}.png`),a=Math.round(e.w),l=Math.round(e.h),p=await Ct();p.busy=(p.busy||0)+1;let w=null,b=null;try{for(let C=0;C<40&&!w;C++)try{w=await(await fetch(`http://127.0.0.1:${p.port}/json/new?about:blank`,{method:"PUT"})).json()}catch{await new Promise(F=>setTimeout(F,50))}if(!w?.webSocketDebuggerUrl)throw new Error("Chrome tab not found");b=new i(w.webSocketDebuggerUrl),await new Promise((C,F)=>{b.onopen=C,b.onerror=()=>F(new Error("DevTools connection failed"))});let j=0,I=new Map,A=null;b.onmessage=C=>{let F=JSON.parse(C.data);F.id&&I.has(F.id)?(I.get(F.id)(F),I.delete(F.id)):F.method==="Page.loadEventFired"&&A?.()};let M=(C,F={})=>new Promise((N,J)=>{let Se=++j;I.set(Se,N),setTimeout(()=>J(new Error("Screenshot timed out")),3e4),b.send(JSON.stringify({id:Se,method:C,params:F}))});await M("Page.enable"),await M("Page.bringToFront"),await M("Emulation.setDeviceMetricsOverride",{width:a,height:l,deviceScaleFactor:2,mobile:!1});let L=new Promise(C=>{A=C,setTimeout(C,12e3)});await M("Page.navigate",{url:t||gn(e)}),await L,await Promise.race([M("Runtime.evaluate",{awaitPromise:!0,expression:"(async () => { try { await document.fonts.ready; } catch {} await Promise.all([...document.images].filter(i => !i.complete).map(i => new Promise(r => { i.onload = i.onerror = r; }))); await new Promise(r => setTimeout(r, 250)); })()"}),new Promise(C=>setTimeout(C,2500))]);let E=await M("Page.captureScreenshot",{format:"png",clip:{x:0,y:0,width:a,height:l,scale:1}});if(!E.result?.data)throw new Error("Screenshot failed");return $.writeFileSync(s,Buffer.from(E.result.data,"base64")),s}catch(j){throw w||Ye(),j}finally{try{b?.close()}catch{}w?.id&&fetch(`http://127.0.0.1:${p.port}/json/close/${w.id}`).catch(()=>{}),p.busy--,clearTimeout(p.idle),p.busy||(p.idle=setTimeout(()=>{W===p&&!p.busy&&Ye()},4*6e4))}}function ie(e){if(x.has(e))return;let t=d.comments.find(o=>o.frameId===e&&o.status==="queued"&&(d.agent.mode!=="inbox"||o.background));t&&Lt(t)}function at(){for(let e of d.frames)ie(e.id);ie(ke)}var kn=new Set(["Reading the code","Editing","Writing","Working\u2026","Setting up an option"]);function B(e,t,o){let i=x.get(e);if(!i)return;i.step=t;let r=o==null&&t&&d.comments.find(s=>s.id===i.commentId);if(r&&!/^(Writing…|Reading your comment…|Your chat picked this up|Working…)$/.test(t)){let s=r.steps||=[],a=s[s.length-1];!kn.has(t)&&(!a||a.s!==t)&&(s.push({t:Date.now(),s:t}),s.length>60&&s.splice(0,s.length-60))}Z({type:"activity",frameId:e,commentId:i.commentId,step:t,...o!=null?{partial:o}:{}}),G?.liveChanged(x)}async function Dt(e){let t=z(e.frameId);if(!(!t||t.kind!=="folder"||!e.options?.length||!X)){if(!e.origThumb&&!e.optionApplied)try{e.origThumb=`/__loop/shots/${m.basename(await ce(t))}`,k()}catch{}for(let o of e.options)try{let i=await ce(t,`http://127.0.0.1:${X}/__loop/optview/${e.id}/${o.id}/${encodeURIComponent(t.entry||"index.html")}`);o.thumb=`/__loop/shots/${m.basename(i)}`,k()}catch{}}}var H={GAP:60,PAD:48,TOP:72},Pe=(e,t)=>t*e.w+(t-1)*H.GAP+H.PAD*2;function bn(e){let t=String(e||""),o=/\b(\d|two|three|four|five|six|a couple of|couple of|a few|few|some|several|more|other|different)\s+(?:[\w-]+\s+){0,3}?(options?|variations?|variants?|alternatives?|versions|directions|takes|ideas|explorations?)\b/i.exec(t);if(o){let i=o[1].toLowerCase(),r={two:2,three:3,four:4,five:5,six:6,"a couple of":2,"couple of":2,"a few":3,few:3,some:2,several:3,more:2,other:2,different:2}[i]??+i;return Math.max(1,Math.min(4,r))}return/\b(options|variations|variants|alternatives|explorations)\b/i.test(t)&&/\b(give|make|create|show|try|explore|generate|come up|want|need|can (we|you)|let'?s|what about)\b/i.test(t)?2:/\b(another|a new|one more|a different|an alternative|a second)\s+(version|variation|variant|option|take|direction|design|layout)\b|\bduplicate (this|it)\b|\b(make|create) a copy\b/i.test(t)?1:0}function yt(e,t,o=new Set){let i=a=>a.y<e.y+e.h&&a.y+a.h>e.y&&a.x+a.w/2>t.x+t.w,r=[...d.frames.filter(a=>a.id!==t.id&&!o.has(a.id)&&i(a)),...d.sections.filter(a=>!o.has(a.id)&&i(a))];if(!r.length)return;let s=e.x+e.w+H.GAP-Math.min(...r.map(a=>a.x));if(s>0)for(let a of r)a.x+=s}function ct(e,t,o){let i=d.sections.find(r=>r.id===e.optionSection);if(i){if(o>(i.slots||0)){let r=new Set([i.id,...(e.options||[]).map(s=>s.frameId)]);yt({x:i.x,y:i.y,w:Pe(t,o),h:i.h},t,r),i.w=Pe(t,o),i.slots=o}}else{let r={x:t.x+t.w+H.GAP+20,y:t.y-H.TOP,w:Pe(t,o),h:t.h+H.TOP+H.PAD};yt(r,t),i={id:R(6),name:`Options \xB7 ${t.name}`,...r,optionsFor:e.id,slots:o,createdAt:Date.now()},d.sections.push(i),e.optionSection=i.id}return i}function Sn(){let e=[...x.entries()].filter(([s])=>s!==ke);if(e.length!==1)return null;let[t,o]=e[0],i=z(t),r=d.comments.find(s=>s.id===o.commentId);return i&&r&&i.kind==="folder"&&!i.optionOf?{c:r,f:i}:null}function xn(e,t,o,i){e.options||=[];let r=String.fromCharCode(65+e.options.length),s=e.options.length,a={id:r,name:i||`Option ${r}`};e.options.push(a);let l=ct(e,t,Math.max(s+1,d.sections.find(w=>w.id===e.optionSection)?.slots||0)),p=q({src:o,name:he(i||`Option ${r}`),w:t.w,h:t.h,x:l.x+H.PAD+s*(t.w+H.GAP),y:t.y});return p.optionOf={cid:e.id,oid:r,from:t.id},p.building=!0,a.dir=o,a.frameId=p.id,a.madeAt=Date.now(),a.touched=!0,k(),p}async function kt(e,t,o){if(e.options||=[],e.options.length>=6)throw new Error("up to 6 options");let i=String.fromCharCode(65+e.options.length),r=e.options.length,s={id:i,name:o||`Option ${i}`,placeholder:!o};e.options.push(s);let a=ct(e,t,Math.max(r+1,d.sections.find(w=>w.id===e.optionSection)?.slots||0)),l=ge(`${t.name} option ${i} ${R(3)}`);await re(t.src,l);let p=q({src:m.join(l,t.entry||"index.html"),name:he(o?`${i} \xB7 ${o}`:`Option ${i}`),w:t.w,h:t.h,x:a.x+H.PAD+r*(t.w+H.GAP),y:t.y});return p.optionOf={cid:e.id,oid:i,from:t.id},p.building=!0,s.dir=l,s.frameId=p.id,s.madeAt=Date.now(),k(),s}function $n(e){if(e.touched)return!0;try{let t=new Set;return ot(e.dir,e.dir,t),[...t].some(o=>$.statSync(m.join(e.dir,o)).mtimeMs>(e.madeAt||0)+800)}catch{return!1}}function vn(e){let t=(e.options||[]).filter(a=>a.frameId);if(!t.length)return;let o=z(e.frameId),i=t.filter(a=>!a.placeholder&&$n(a)),r=new Set(t.filter(a=>!i.includes(a)).map(a=>a.frameId));for(let a of r)_e(a);d.frames=d.frames.filter(a=>!r.has(a.id)),e.options=e.options.filter(a=>!a.frameId||i.includes(a));let s=d.sections.find(a=>a.id===e.optionSection);i.forEach((a,l)=>{let p=d.frames.find(w=>w.id===a.frameId);p&&(delete p.building,s&&o&&(p.x=s.x+H.PAD+l*(o.w+H.GAP)))}),s&&!i.length?(d.sections=d.sections.filter(a=>a!==s),delete e.optionSection):s&&o&&(s.w=Pe(o,i.length),s.slots=i.length)}async function In(e){let t=z(e.frameId),o=(e.options||[]).filter(s=>s.files?.length&&!s.frameId);if(!t||t.kind!=="folder"||!o.length)return!1;let i=ct(e,t,o.length),r=0;for(let s of o){let a=ge(`${t.name} ${s.id} ${s.name}`);try{await re(t.src,a),await re(s.dir,a)}catch{continue}let l=q({src:m.join(a,t.entry||"index.html"),name:he(`${s.id} \xB7 ${s.name}`),w:t.w,h:t.h,x:i.x+H.PAD+r++*(t.w+H.GAP),y:t.y});l.optionOf={cid:e.id,oid:s.id,from:t.id},s.frameId=l.id}return k(),r>0}function Y(e,t,o){if(vn(e),e.options?.length)for(let s of e.options)s.files=(()=>{try{let a=new Set;return ot(s.dir,s.dir,a),[...a]}catch{return[]}})();e.options?.length&&(e.options=e.options.filter(s=>s.files.length)),e.options?.some(s=>!s.frameId)&&t==="done"&&setTimeout(async()=>{await In(e).catch(()=>!1)||Dt(e)},50);let i=x.get(e.frameId);x.delete(e.frameId),e.status=i?.steered?"interrupted":i?.canceled?"canceled":t,e.reply=i?.steered?"Interrupted to take your next message.":i?.canceled?"Stopped.":o,e.finishedAt=Date.now(),ae.set(e.frameId,(ae.get(e.frameId)||0)+1),Me.add(e.frameId),k(),Z({type:"frame-changed",frameId:e.frameId,done:!0,commentId:e.id,status:e.status});let r=d.frames.find(s=>s.id===e.frameId);r&&G?.frameChanged(r),G?.liveChanged(x),setTimeout(()=>ie(e.frameId),50)}function be(){let e=d.agent.runner;return e&&fe[e]?.bin?e:Oe?"claude":et?"codex":"claude"}var U=new Map,Tn=3,On=10*6e4;function pe(e){clearTimeout(e.idle),e.dead=!0;try{e.child.kill("SIGTERM")}catch{}U.get(e.frameId)===e&&U.delete(e.frameId)}function Mt(e){clearTimeout(e.idle),e.idle=setTimeout(()=>{e.turn||pe(e)},On)}function Xe(e,t=!1){let o=U.get(e.id),i=$e("claude"),r=`${i.model}|${i.effort}`;if(o&&!o.dead){if(o.turn||o.cfgKey===r)return o;!o.turns&&!o.resuming&&delete e.sessionId,pe(o)}let s=["-p","--input-format","stream-json","--output-format","stream-json","--verbose","--include-partial-messages","--permission-mode","acceptEdits","--allowedTools",[`Read,Edit,MultiEdit,Write,Glob,Grep,TodoWrite,WebFetch,WebSearch,Bash(node ${K} new:*),Bash(node ${K} add-route:*),Bash(node ${K} option:*),Bash(node ${K} shot:*)`,...jn()].join(","),"--add-dir",O,...An().flatMap(I=>["--add-dir",I])],a=!!(e.sessionId&&!t);a&&s.push("--resume",e.sessionId),i.model&&s.push("--model",i.model),i.effort&&s.push("--effort",i.effort),e.kind!=="canvas"&&s.push("--append-system-prompt",Pt(e));let l=e.kind==="folder"?e.src:de,p=Te(Oe,s,{cwd:l,env:process.env,stdio:["pipe","pipe","pipe"]}),w={frameId:e.id,child:p,turn:null,turns:0,resuming:a,idle:null,dead:!1,stderr:"",cfgKey:r};U.set(e.id,w),p.stdin.on("error",()=>{});let b="";p.stdout.on("data",I=>{b+=I;let A;for(;(A=b.indexOf(`
24
- `))>=0;){let M=b.slice(0,A);if(b=b.slice(A+1),!M.trim())continue;let L;try{L=JSON.parse(M)}catch{continue}process.env.LOOP_DEBUG&&console.log(Date.now()%1e6,L.type,L.subtype||L.event?.type||"",L.event?.content_block?.type||L.event?.delta?.type||"");try{Cn(w,e,L)}catch(E){console.error(E)}}}),p.stderr.on("data",I=>{w.stderr=(w.stderr+I).slice(-2e3)}),p.on("error",I=>{w.stderr+=I.message}),p.on("close",I=>{w.dead=!0,clearTimeout(w.idle),U.get(e.id)===w&&U.delete(e.id);let A=w.turn;w.turn=null;let M=w.resuming&&!w.turns;if(!A){M&&(e.sessionId=null);return}let L=A.c,E=x.get(e.id);if(E?.commentId===L.id){if(!E.canceled&&M)return e.sessionId=null,x.delete(e.id),Lt(L,!0);E.canceled&&!E.steered&&!w.turns&&!w.resuming&&(e.sessionId=null),Y(L,"error",String(A.lastText&&E.canceled?A.lastText:w.stderr.trim()||`Agent exited with code ${I}`).slice(-400))}}),Mt(w);let j=[...U.values()].filter(I=>!I.turn&&I!==w);for(;U.size>Tn&&j.length;)pe(j.shift());return w}var jn=()=>(d.agent.allow||[]).filter(e=>!e.startsWith("dir:")),An=()=>(d.agent.allow||[]).filter(e=>e.startsWith("dir:")).map(e=>e.slice(4)),_n=/^(cd|tail|head|grep|sort|uniq|wc|cat|echo|true|sleep|tee|xargs)$/;function Pn(e){let t=String(e.tool_name||""),o=e.tool_input||{};if(t==="Bash"){let r=String(o.command||"").split(/&&|\|\||;|\|/).map(p=>p.trim()).filter(Boolean),s=r.find(p=>!_n.test(p.split(/\s+/)[0]))||r[0];if(!s)return null;let a=[];for(let p of s.split(/\s+/)){if(a.length===3||/^\d*[<>]|[<>&|$`'"*(){};]/.test(p))break;a.push(p)}if(!a.length)return null;let l=a.join(" ");return{tool:t,label:l,rule:`Bash(${l}:*)`,cmd:String(o.command).slice(0,300)}}let i=o.file_path||o.notebook_path||o.path;if(/^(Read|Edit|MultiEdit|Write|NotebookEdit|Glob|Grep)$/.test(t)&&i){let r=m.resolve(String(i)),s=/^(Glob|Grep)$/.test(t)?r:m.dirname(r);return{tool:t,label:`${/^(Read|Glob|Grep)$/.test(t)?"Read":"Edit"} files in ${s.replace(se.homedir(),"~")}`,rule:`dir:${s}`,cmd:String(i)}}return t?{tool:t,label:t.replace(/^mcp__/,"").replace(/__/g," \xB7 "),rule:t}:null}function En(e){let t=[],o=new Set(d.agent.allow||[]);for(let i of e||[]){let r=Pn(i);r&&!o.has(r.rule)&&!t.some(s=>s.rule===r.rule)&&t.push(r)}return t}function Cn(e,t,o){o.session_id&&(t.sessionId=o.session_id);let i=e.turn;if(!i)return;let r=i.c,s=o.type==="system"&&o.model||o.type==="assistant"&&o.message?.model;if(s&&r.model!==s&&(r.model=s,d.agent.lastModel=s,k()),r.t&&r.t.first==null&&o.type==="stream_event"&&(r.t.first=Date.now()-r.createdAt),r.t&&r.t.tool==null&&o.type==="stream_event"&&o.event?.content_block?.type==="tool_use"&&(r.t.tool=Date.now()-r.createdAt),o.type==="stream_event"){let a=o.event||{};if(a.type==="message_start"&&x.get(t.id)?.step==="Reading your comment\u2026"&&B(t.id,"Thinking\u2026"),a.type==="content_block_start"){let l=a.content_block||{};l.type==="thinking"?B(t.id,"Thinking\u2026"):l.type==="tool_use"?(i.partial="",B(t.id,mt(l.name,{}))):l.type==="text"&&(i.partial="")}else a.type==="content_block_delta"&&a.delta?.type==="text_delta"&&(i.partial+=a.delta.text,i.flush||(i.flush=setTimeout(()=>{i.flush=null,e.turn===i&&B(t.id,"Writing\u2026",i.partial.slice(-4e3))},90)))}else if(o.type==="assistant"){for(let a of o.message?.content||[])if(a.type==="tool_use")B(t.id,mt(a.name,a.input));else if(a.type==="text"&&a.text.trim()){i.lastText=a.text.trim();let l=r.steps||=[];l.push({t:Date.now(),s:i.lastText.slice(0,600),say:!0}),l.length>60&&l.splice(0,l.length-60),i.partial="",k()}}else if(o.type==="result"){clearTimeout(i.flush),e.turn=null,e.turns++,Mt(e);let a=o.result??i.lastText,l=r.steps?.[r.steps.length-1];l?.say&&String(a||"").trim().startsWith(l.s.slice(0,80))&&r.steps.pop();let p=!!o.is_error||(o.permission_denials||[]).some(b=>/^(Edit|MultiEdit|Write)$/.test(b.tool_name)),w=En(o.permission_denials);if(w.length?r.blocked=w:delete r.blocked,e.restart&&pe(e),x.get(t.id)?.commentId!==r.id)return;a!=null&&!p?Y(r,"done",String(a).trim().slice(0,4e3)||"Done."):Y(r,"error",String(a||e.stderr.trim()||"Something went wrong.").slice(-400))}}function Dn(e){if(!Oe||be()!=="claude"||x.has(e.id)||d.agent.mode==="inbox"&&Le())return!1;try{return Xe(e),!0}catch{return!1}}async function Lt(e,t=!1){let o=z(e.frameId);if(!o)return;if(be()==="codex")return Ft(e,o,t);if(!Oe)return x.set(o.id,{commentId:e.id,step:"",startedAt:Date.now()}),Y(e,"error",'No agent found to run this. Install Claude Code or Codex, or switch to "Your agent session" in the top-right menu.');let i={commentId:e.id,step:"Reading your comment\u2026",startedAt:e.startedAt&&t?e.startedAt:Date.now()};x.set(o.id,i),e.startedAt=i.startedAt,e.status="working",k();let r,s=!!U.get(o.id)&&!U.get(o.id).dead;try{r=Xe(o,t)}catch(p){return Y(e,"error",`Couldn't start the agent: ${p.message}`)}r.warmed=s,i.child=r.child;let a=!t&&!e.snapshot?await it(o,e.id):e.snapshot;if(e.snapshot=a,x.get(o.id)!==i)return;if(i.canceled)return Y(e,"canceled","Stopped.");if(r.dead){try{r=Xe(o,!0)}catch(p){return Y(e,"error",`Couldn't start the agent: ${p.message}`)}i.child=r.child}let l=e.kind==="create"||o.kind==="canvas"?st(o,e):_t(o,e).message;r.turn={c:e,lastText:"",partial:"",flush:null},clearTimeout(r.idle),e.t={sent:Date.now()-e.createdAt,warm:!!r.warmed},r.child.stdin.write(JSON.stringify({type:"user",message:{role:"user",content:[{type:"text",text:l}]}})+`
25
- `)}async function Ft(e,t,o=!1){x.set(t.id,{commentId:e.id,step:"Starting Codex\u2026",startedAt:Date.now()}),e.startedAt=Date.now(),e.status="working",k(),!o&&!e.snapshot&&(e.snapshot=await it(t,e.id));let i=st(t,e),r=t.kind==="folder"?t.src:de,{model:s,effort:a}=$e("codex"),l=["--json","-c","sandbox_workspace_write.network_access=true",...s?["-m",s]:[],...a?["-c",`model_reasoning_effort="${a}"`]:[]],p=!!(t.codexSession&&!o&&e.kind!=="create"),w=p?["exec","resume",...l,"--skip-git-repo-check","-c",'sandbox_mode="workspace-write"',"-c",`sandbox_workspace_write.writable_roots=${JSON.stringify([r,O])}`,t.codexSession,"-"]:["exec",...l,"-s","workspace-write","--skip-git-repo-check","-C",r,"--add-dir",O,"-"];s&&(e.model=s,d.agent.lastModel=s,k());let b;try{b=Te(et,w,{cwd:r,env:process.env,stdio:["pipe","pipe","pipe"]})}catch(E){return Y(e,"error",`Couldn't start Codex: ${E.message}`)}x.get(t.id).child=b,b.stdin.on("error",()=>{}),b.stdin.end(i);let j="",I="",A=null,M=!1,L="";b.stdout.on("data",E=>{j+=E;let C;for(;(C=j.indexOf(`
26
- `))>=0;){let F=j.slice(0,C);j=j.slice(C+1);let N;try{N=JSON.parse(F)}catch{continue}let J=N.item||{};N.type==="thread.started"&&N.thread_id&&e.kind!=="create"?t.codexSession=N.thread_id:J.type==="command_execution"&&N.type==="item.started"?B(t.id,/\bcat |sed -n|rg |ls\b|grep /.test(J.command||"")?"Reading the code":"Running a command"):J.type==="file_change"?B(t.id,`Editing ${m.basename(J.changes?.[0]?.path||"")}`.trim()):J.type==="agent_message"&&J.text?.trim()?(I=J.text.trim(),B(t.id,"Thinking\u2026")):J.type==="reasoning"?B(t.id,"Thinking\u2026"):N.type==="turn.completed"?M=!0:(N.type==="turn.failed"||N.type==="error")&&(A=N.error?.message||N.message||"Codex failed")}}),b.stderr.on("data",E=>{L=(L+E).slice(-2e3)}),b.on("error",E=>{L+=E.message}),b.on("close",E=>{if(!x.get(t.id)?.canceled&&!M&&p)return t.codexSession=null,x.delete(t.id),Ft(e,t,!0);M&&!A?Y(e,"done",I.slice(0,4e3)||"Done."):Y(e,"error",String(A||L.trim()||`Codex exited with code ${E}`).slice(-400))})}function Mn(){for(let e of Object.values(se.networkInterfaces()))for(let t of e||[])if(t.family==="IPv4"&&!t.internal)return t.address;return null}var X=0;function Ze(){let e=Mn();return{lan:e?`http://${e}:${X}${ue}/?t=${d.token}`:null,public:ne?`${ne}${ue}/?t=${d.token}`:null,tunnelAvailable:!!me("cloudflared")}}function Ln(){return new Promise((e,t)=>{if(ne)return e(ne);let o=me("cloudflared");if(!o)return t(new Error("Public links need cloudflared. Install it with: brew install cloudflared"));le=Te(o,["tunnel","--no-autoupdate","--url",`http://localhost:${X}`],{stdio:["ignore","pipe","pipe"]});let i=r=>{let s=String(r).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);s&&!ne&&(ne=s[0],k(),e(ne))};le.stdout.on("data",i),le.stderr.on("data",i),le.on("close",()=>{ne=null,le=null,k()}),setTimeout(()=>t(new Error("The tunnel took too long to start")),2e4)})}var Fn={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".woff2":"font/woff2",".woff":"font/woff",".ttf":"font/ttf",".ico":"image/x-icon",".mp4":"video/mp4",".txt":"text/plain; charset=utf-8"};function Rt(e,t){let o=e.socket.remoteAddress||"",i=o==="127.0.0.1"||o==="::1"||o==="::ffff:127.0.0.1",r=e.headers["x-forwarded-for"]||e.headers["cf-connecting-ip"];if(i&&!r)return"owner";let s=/(?:^|;\s*)loop_t=([^;]+)/.exec(e.headers.cookie||"")?.[1];return t.searchParams.get("t")===d.token||s===d.token?"guest":null}async function D(e){let t=[],o=0;for await(let r of e){if(o+=r.length,o>15e6)throw new Error("too large");t.push(r)}let i=Buffer.concat(t).toString("utf8");return i?JSON.parse(i):{}}function u(e,t,o){e.writeHead(t,{"content-type":"application/json","cache-control":"no-store"}),e.end(JSON.stringify(o))}async function te(e,t,o={}){try{if((await _.stat(t)).isDirectory())return te(e,m.join(t,"index.html"),o);e.writeHead(200,{"content-type":Fn[m.extname(t).toLowerCase()]||"application/octet-stream","cache-control":"no-store",...o}),$.createReadStream(t).pipe(e)}catch{e.writeHead(404),e.end("Not found")}}var oe=(e,t)=>t===e||t.startsWith(e+m.sep),Ee=xt.createServer(async(e,t)=>{let o=new URL(e.url,"http://x"),i;try{i=decodeURIComponent(o.pathname)}catch{return t.writeHead(400),t.end()}let r=Rt(e,o);if(!r)return t.writeHead(403,{"content-type":"text/plain"}),t.end("This Loop canvas is private. Ask the host for a share link.");let s=r==="owner";if(i!==ue&&!i.startsWith(ue+"/"))return d.proxy?nn(e,t,d.proxy):i==="/"?(t.writeHead(302,{location:`${ue}/${o.search}`}),t.end()):(t.writeHead(404),t.end("Not found"));i=i.slice(ue.length)||"/";try{if(i==="/"||i==="/index.html"){let n=o.searchParams.get("t")?{"set-cookie":`loop_t=${d.token}; Path=/; HttpOnly; SameSite=Lax`}:{};return te(t,m.join(Ne,"index.html"),n)}if(i.startsWith("/app/")){let n=m.join(Ne,m.normalize(i.slice(5)));return oe(Ne,n)?te(t,n):(t.writeHead(400),t.end())}let a=/^\/optview\/([A-Za-z0-9_-]+)\/([A-F])\/(.*)$/.exec(i);if(a){let n=d.comments.find(v=>v.id===a[1]),c=n&&z(n.frameId),f=n?.options?.find(v=>v.id===a[2]);if(!c||!f||c.kind!=="folder")return t.writeHead(404),t.end();let h=m.normalize(decodeURIComponent(a[3]||c.entry)),g=m.join(f.dir,h),y=m.join(c.src,h);return!oe(f.dir,g)||!oe(c.src,y)?(t.writeHead(400),t.end()):te(t,$.existsSync(g)?g:y)}let l=/^\/hist\/([A-Za-z0-9_-]+)\/(.*)$/.exec(i);if(l){let n=m.join(O,"history",l[1]),c=m.join(n,m.normalize(l[2]||"index.html"));return oe(n,c)?te(t,c):(t.writeHead(400),t.end())}let p=/^\/poster\/([A-Za-z0-9_-]+)$/.exec(i);if(p){let n=d.frames.find(c=>c.id===p[1]);if(!n)return t.writeHead(404),t.end();try{return te(t,await wn(n),{"cache-control":"private, max-age=3600"})}catch{return t.writeHead(404),t.end()}}let w=/^\/shots\/([\w.-]+\.png)$/.exec(i);if(w)return te(t,m.join(O,"shots",w[1]));let b=/^\/attachments\/([A-Za-z0-9_-]+)\/([^/]+)$/.exec(i);if(b){let n=m.join(O,"attachments",b[1]),c=m.join(n,decodeURIComponent(b[2]));return oe(n,c)?te(t,c):(t.writeHead(400),t.end())}let j=/^\/f\/([^/]+)\/(.*)$/.exec(i);if(j){let n=d.frames.find(f=>f.id===j[1]);if(!n||n.kind!=="folder"&&n.kind!=="frozen")return t.writeHead(404),t.end();let c=m.join(n.src,m.normalize(j[2]||n.entry));return oe(n.src,c)?te(t,c):(t.writeHead(400),t.end())}if(i==="/api/ping")return u(t,200,{ok:!0,name:d.name,pid:process.pid});if(i==="/api/state")return u(t,200,It(r));if(i==="/api/events"){t.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-store",connection:"keep-alive"});let n={res:t,role:r};ze.add(n),He(t,Ot(n));let c=setInterval(()=>t.write(`: ka
22
+ ${e.reference}`:e.message}var be="__canvas";function H(e){return e!==be?d.frames.find(t=>t.id===e):(d.canvasFrame||={id:be,name:"Canvas",kind:"canvas",src:le,entry:"",w:0,h:0},d.canvasFrame)}var Ye=0,Re=()=>Date.now()-Ye<45e3;function kn(e){let t=S.get(e.frameId),o=t&&d.comments.find(s=>s.id===t.commentId),i=e.parentId||e.id;if(e.status!=="queued"||!o||o.id!==i&&o.parentId!==i)return!1;e.steeredFrom=o.id,d.comments.splice(d.comments.indexOf(e),1);let r=d.comments.findIndex(s=>s.frameId===e.frameId&&s.status==="queued");return d.comments.splice(r<0?d.comments.length:r,0,e),t.steered=!0,t.canceled=!0,t.child?t.child.kill("SIGTERM"):V(o,"canceled","Stopped."),k(),!0}function Mt(e){return e.kind==="folder"?e.src:le}async function wt(e){let t=new Set;try{await Le(e,e,t)}catch{}return[...t]}async function yt(e,t){let o=H(e.frameId);if(!o)throw new Error("no frame");if(e.optionChosen)throw new Error("an option was already chosen");let i=Mt(o),r=m.join(O,"options",e.id,"_original"),s=new Set;for(let p of e.options||[])for(let g of await wt(p.dir))s.add(g);if(!$.existsSync(r)){await P.mkdir(r,{recursive:!0});let p=[];for(let g of s){let x=m.join(i,g);oe(i,x)&&($.existsSync(x)?(await P.mkdir(m.dirname(m.join(r,g)),{recursive:!0}),await P.copyFile(x,m.join(r,g))):p.push(g))}await P.writeFile(m.join(r,".missing.json"),JSON.stringify(p))}let a=JSON.parse(await P.readFile(m.join(r,".missing.json"),"utf8").catch(()=>"[]"));for(let p of s){let g=m.join(i,p);oe(i,g)&&(a.includes(p)?await P.rm(g,{force:!0}):$.existsSync(m.join(r,p))&&await P.copyFile(m.join(r,p),g))}let l=t&&(e.options||[]).find(p=>p.id===t);if(t&&!l)throw new Error("no such option");if(l)for(let p of await wt(l.dir)){let g=m.join(i,p);oe(i,g)&&(await P.mkdir(m.dirname(g),{recursive:!0}),await P.copyFile(m.join(l.dir,p),g))}e.optionApplied=l?l.id:null,k(),Z({type:"frame-changed",frameId:o.id})}var Ne=process.env.LOOP_CHROME||["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge","/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",m.join(ae.homedir(),"Applications/Google Chrome.app/Contents/MacOS/Google Chrome")].find(e=>$.existsSync(e))||me("google-chrome")||me("chromium")||me("chromium-browser");function bn(e){return e.kind==="folder"||e.kind==="frozen"?`http://127.0.0.1:${K}/__loop/f/${e.id}/${encodeURIComponent(e.entry||"index.html")}`:e.kind==="route"?`http://127.0.0.1:${K}${e.src}`:e.src}var G=null;function Xe(){let e=G;if(G=null,!!e){clearTimeout(e.idle);try{e.child.kill("SIGKILL")}catch{}$.rm(e.profile,{recursive:!0,force:!0},()=>{})}}async function Lt(){if(G)return clearTimeout((await G).idle),G;G=(async()=>{let t=$.mkdtempSync(m.join(ae.tmpdir(),"loop-shot-")),o=Oe(Ne,["--headless=new","--use-angle=swiftshader","--enable-unsafe-swiftshader","--ignore-gpu-blocklist","--hide-scrollbars","--no-first-run","--no-default-browser-check","--mute-audio",`--user-data-dir=${t}`,"--remote-debugging-port=0","--window-size=1280,900","about:blank"],{stdio:"ignore"}),i={child:o,profile:t,port:null,idle:null};o.on("exit",()=>{G&&(G===e||G===i)&&(G=null,$.rm(t,{recursive:!0,force:!0},()=>{}))});for(let r=0;r<100&&!i.port;r++)try{i.port=$.readFileSync(m.join(t,"DevToolsActivePort"),"utf8").split(`
23
+ `)[0]}catch{await new Promise(s=>setTimeout(s,50))}if(!i.port){try{o.kill("SIGKILL")}catch{}throw new Error("Chrome did not start")}return i})();let e=G;try{let t=await e;return G===e&&(G=t),t}catch(t){throw G===e&&(G=null),t}}var $e=new Map,Fe=new Set;async function xn(e){let t=`${e.w}x${e.h}`,o=$e.get(e.id);if(o&&!(o instanceof Promise)&&o.size===t&&!Fe.has(e.id)&&$.existsSync(o.file))return o.file;if(o instanceof Promise)return(await o).file;Fe.delete(e.id);let i=de(e).then(r=>{let s=$e.get(e.id),a={file:r,size:t};return $e.set(e.id,a),a}).catch(r=>{throw $e.delete(e.id),r});return $e.set(e.id,i),(await i).file}var kt=Promise.resolve();function de(e,t,o){let i=kt.then(()=>Sn(e,t,o));return kt=i.catch(()=>{}),i}async function Sn(e,t,o){if(!Ne)throw new Error("No Chrome, Edge, Brave or Chromium found for screenshots (set LOOP_CHROME to one).");let i=globalThis.WebSocket;if(!i)throw new Error("Screenshots need Node 22 or newer");let r=m.join(O,"shots");$.mkdirSync(r,{recursive:!0});let s=m.join(r,o||`${e.id}-${Date.now()}${N(3)}.png`),a=Math.round(e.w),l=Math.round(e.h),p=await Lt();p.busy=(p.busy||0)+1;let g=null,x=null;try{for(let D=0;D<40&&!g;D++)try{g=await(await fetch(`http://127.0.0.1:${p.port}/json/new?about:blank`,{method:"PUT"})).json()}catch{await new Promise(R=>setTimeout(R,50))}if(!g?.webSocketDebuggerUrl)throw new Error("Chrome tab not found");x=new i(g.webSocketDebuggerUrl),await new Promise((D,R)=>{x.onopen=D,x.onerror=()=>R(new Error("DevTools connection failed"))});let j=0,I=new Map,_=null;x.onmessage=D=>{let R=JSON.parse(D.data);R.id&&I.has(R.id)?(I.get(R.id)(R),I.delete(R.id)):R.method==="Page.loadEventFired"&&_?.()};let A=(D,R={})=>new Promise((W,Y)=>{let Se=++j;I.set(Se,W),setTimeout(()=>Y(new Error("Screenshot timed out")),3e4),x.send(JSON.stringify({id:Se,method:D,params:R}))});await A("Page.enable"),await A("Page.bringToFront"),await A("Emulation.setDeviceMetricsOverride",{width:a,height:l,deviceScaleFactor:2,mobile:!1});let F=new Promise(D=>{_=D,setTimeout(D,12e3)});await A("Page.navigate",{url:t||bn(e)}),await F,await Promise.race([A("Runtime.evaluate",{awaitPromise:!0,expression:"(async () => { try { await document.fonts.ready; } catch {} await Promise.all([...document.images].filter(i => !i.complete).map(i => new Promise(r => { i.onload = i.onerror = r; }))); await new Promise(r => setTimeout(r, 250)); })()"}),new Promise(D=>setTimeout(D,2500))]);let C=await A("Page.captureScreenshot",{format:"png",clip:{x:0,y:0,width:a,height:l,scale:1}});if(!C.result?.data)throw new Error("Screenshot failed");return $.writeFileSync(s,Buffer.from(C.result.data,"base64")),s}catch(j){throw g||Xe(),j}finally{try{x?.close()}catch{}g?.id&&fetch(`http://127.0.0.1:${p.port}/json/close/${g.id}`).catch(()=>{}),p.busy--,clearTimeout(p.idle),p.busy||(p.idle=setTimeout(()=>{G===p&&!p.busy&&Xe()},4*6e4))}}function ie(e){if(S.has(e))return;let t=d.comments.find(o=>o.frameId===e&&o.status==="queued"&&(d.agent.mode!=="inbox"||o.background));t&&Nt(t)}function ct(){for(let e of d.frames)ie(e.id);ie(be)}var $n=new Set(["Reading the code","Editing","Writing","Working\u2026","Setting up an option"]);function J(e,t,o){let i=S.get(e);if(!i)return;i.step=t;let r=o==null&&t&&d.comments.find(s=>s.id===i.commentId);if(r&&!/^(Writing…|Reading your comment…|Your chat picked this up|Working…)$/.test(t)){let s=r.steps||=[],a=s[s.length-1];!$n.has(t)&&(!a||a.s!==t)&&(s.push({t:Date.now(),s:t}),s.length>60&&s.splice(0,s.length-60))}Z({type:"activity",frameId:e,commentId:i.commentId,step:t,...o!=null?{partial:o}:{}}),z?.liveChanged(S)}async function Ft(e){let t=H(e.frameId);if(!(!t||t.kind!=="folder"||!e.options?.length||!K)){if(!e.origThumb&&!e.optionApplied)try{e.origThumb=`/__loop/shots/${m.basename(await de(t))}`,k()}catch{}for(let o of e.options)try{let i=await de(t,`http://127.0.0.1:${K}/__loop/optview/${e.id}/${o.id}/${encodeURIComponent(t.entry||"index.html")}`);o.thumb=`/__loop/shots/${m.basename(i)}`,k()}catch{}}}var U={GAP:60,PAD:48,TOP:72},Ee=(e,t)=>t*e.w+(t-1)*U.GAP+U.PAD*2;function vn(e){let t=String(e||""),o=/\b(\d|two|three|four|five|six|a couple of|couple of|a few|few|some|several|more|other|different)\s+(?:[\w-]+\s+){0,3}?(options?|variations?|variants?|alternatives?|versions|directions|takes|ideas|explorations?)\b/i.exec(t);if(o){let i=o[1].toLowerCase(),r={two:2,three:3,four:4,five:5,six:6,"a couple of":2,"couple of":2,"a few":3,few:3,some:2,several:3,more:2,other:2,different:2}[i]??+i;return Math.max(1,Math.min(4,r))}return/\b(options|variations|variants|alternatives|explorations)\b/i.test(t)&&/\b(give|make|create|show|try|explore|generate|come up|want|need|can (we|you)|let'?s|what about)\b/i.test(t)?2:/\b(another|a new|one more|a different|an alternative|a second)\s+(version|variation|variant|option|take|direction|design|layout)\b|\bduplicate (this|it)\b|\b(make|create) a copy\b/i.test(t)?1:0}function bt(e,t,o=new Set){let i=a=>a.y<e.y+e.h&&a.y+a.h>e.y&&a.x+a.w/2>t.x+t.w,r=[...d.frames.filter(a=>a.id!==t.id&&!o.has(a.id)&&i(a)),...d.sections.filter(a=>!o.has(a.id)&&i(a))];if(!r.length)return;let s=e.x+e.w+U.GAP-Math.min(...r.map(a=>a.x));if(s>0)for(let a of r)a.x+=s}function dt(e,t,o){let i=d.sections.find(r=>r.id===e.optionSection);if(i){if(o>(i.slots||0)){let r=new Set([i.id,...(e.options||[]).map(s=>s.frameId)]);bt({x:i.x,y:i.y,w:Ee(t,o),h:i.h},t,r),i.w=Ee(t,o),i.slots=o}}else{let r={x:t.x+t.w+U.GAP+20,y:t.y-U.TOP,w:Ee(t,o),h:t.h+U.TOP+U.PAD};bt(r,t),i={id:N(6),name:`Options \xB7 ${t.name}`,...r,optionsFor:e.id,slots:o,createdAt:Date.now()},d.sections.push(i),e.optionSection=i.id}return i}function In(){let e=[...S.entries()].filter(([s])=>s!==be);if(e.length!==1)return null;let[t,o]=e[0],i=H(t),r=d.comments.find(s=>s.id===o.commentId);return i&&r&&i.kind==="folder"&&!i.optionOf?{c:r,f:i}:null}function Tn(e,t,o,i){e.options||=[];let r=String.fromCharCode(65+e.options.length),s=e.options.length,a={id:r,name:i||`Option ${r}`};e.options.push(a);let l=dt(e,t,Math.max(s+1,d.sections.find(g=>g.id===e.optionSection)?.slots||0)),p=Q({src:o,name:he(i||`Option ${r}`),w:t.w,h:t.h,x:l.x+U.PAD+s*(t.w+U.GAP),y:t.y});return p.optionOf={cid:e.id,oid:r,from:t.id},p.building=!0,a.dir=o,a.frameId=p.id,a.madeAt=Date.now(),a.touched=!0,k(),p}async function xt(e,t,o){if(e.options||=[],e.options.length>=6)throw new Error("up to 6 options");let i=String.fromCharCode(65+e.options.length),r=e.options.length,s={id:i,name:o||`Option ${i}`,placeholder:!o};e.options.push(s);let a=dt(e,t,Math.max(r+1,d.sections.find(g=>g.id===e.optionSection)?.slots||0)),l=we(`${t.name} option ${i} ${N(3)}`);await se(t.src,l);let p=Q({src:m.join(l,t.entry||"index.html"),name:he(o?`${i} \xB7 ${o}`:`Option ${i}`),w:t.w,h:t.h,x:a.x+U.PAD+r*(t.w+U.GAP),y:t.y});return p.optionOf={cid:e.id,oid:i,from:t.id},p.building=!0,s.dir=l,s.frameId=p.id,s.madeAt=Date.now(),k(),s}function On(e){if(e.touched)return!0;try{let t=new Set;return it(e.dir,e.dir,t),[...t].some(o=>$.statSync(m.join(e.dir,o)).mtimeMs>(e.madeAt||0)+800)}catch{return!1}}function jn(e){let t=(e.options||[]).filter(a=>a.frameId);if(!t.length)return;let o=H(e.frameId),i=t.filter(a=>!a.placeholder&&On(a)),r=new Set(t.filter(a=>!i.includes(a)).map(a=>a.frameId));for(let a of r)Pe(a);d.frames=d.frames.filter(a=>!r.has(a.id)),e.options=e.options.filter(a=>!a.frameId||i.includes(a));let s=d.sections.find(a=>a.id===e.optionSection);i.forEach((a,l)=>{let p=d.frames.find(g=>g.id===a.frameId);p&&(delete p.building,s&&o&&(p.x=s.x+U.PAD+l*(o.w+U.GAP)))}),s&&!i.length?(d.sections=d.sections.filter(a=>a!==s),delete e.optionSection):s&&o&&(s.w=Ee(o,i.length),s.slots=i.length)}async function An(e){let t=H(e.frameId),o=(e.options||[]).filter(s=>s.files?.length&&!s.frameId);if(!t||t.kind!=="folder"||!o.length)return!1;let i=dt(e,t,o.length),r=0;for(let s of o){let a=we(`${t.name} ${s.id} ${s.name}`);try{await se(t.src,a),await se(s.dir,a)}catch{continue}let l=Q({src:m.join(a,t.entry||"index.html"),name:he(`${s.id} \xB7 ${s.name}`),w:t.w,h:t.h,x:i.x+U.PAD+r++*(t.w+U.GAP),y:t.y});l.optionOf={cid:e.id,oid:s.id,from:t.id},s.frameId=l.id}return k(),r>0}function V(e,t,o){if(jn(e),e.options?.length)for(let s of e.options)s.files=(()=>{try{let a=new Set;return it(s.dir,s.dir,a),[...a]}catch{return[]}})();e.options?.length&&(e.options=e.options.filter(s=>s.files.length)),e.options?.some(s=>!s.frameId)&&t==="done"&&setTimeout(async()=>{await An(e).catch(()=>!1)||Ft(e)},50);let i=S.get(e.frameId);S.delete(e.frameId),e.status=i?.steered?"interrupted":i?.canceled?"canceled":t,e.reply=i?.steered?"Interrupted to take your next message.":i?.canceled?"Stopped.":o,e.finishedAt=Date.now(),ce.set(e.frameId,(ce.get(e.frameId)||0)+1),Fe.add(e.frameId),k(),Z({type:"frame-changed",frameId:e.frameId,done:!0,commentId:e.id,status:e.status});let r=d.frames.find(s=>s.id===e.frameId);r&&z?.frameChanged(r),z?.liveChanged(S),setTimeout(()=>ie(e.frameId),50)}function xe(){let e=d.agent.runner;return e&&ue[e]?.bin?e:je?"claude":tt?"codex":"claude"}var B=new Map,_n=3,Pn=10*6e4;function pe(e){clearTimeout(e.idle),e.dead=!0;try{e.child.kill("SIGTERM")}catch{}B.get(e.frameId)===e&&B.delete(e.frameId)}function Rt(e){clearTimeout(e.idle),e.idle=setTimeout(()=>{e.turn||pe(e)},Pn)}function Ze(e,t=!1){let o=B.get(e.id),i=ve("claude"),r=`${i.model}|${i.effort}`;if(o&&!o.dead){if(o.turn||o.cfgKey===r)return o;!o.turns&&!o.resuming&&delete e.sessionId,pe(o)}let s=["-p","--input-format","stream-json","--output-format","stream-json","--verbose","--include-partial-messages","--permission-mode","acceptEdits","--allowedTools",[`Read,Edit,MultiEdit,Write,Glob,Grep,TodoWrite,WebFetch,WebSearch,Bash(node ${X} new:*),Bash(node ${X} add-route:*),Bash(node ${X} option:*),Bash(node ${X} shot:*)`,...En()].join(","),"--add-dir",O,...Cn().flatMap(I=>["--add-dir",I])],a=!!(e.sessionId&&!t);a&&s.push("--resume",e.sessionId),i.model&&s.push("--model",i.model),i.effort&&s.push("--effort",i.effort),e.kind!=="canvas"&&s.push("--append-system-prompt",Dt(e));let l=e.kind==="folder"?e.src:le,p=Oe(je,s,{cwd:l,env:process.env,stdio:["pipe","pipe","pipe"]}),g={frameId:e.id,child:p,turn:null,turns:0,resuming:a,idle:null,dead:!1,stderr:"",cfgKey:r};B.set(e.id,g),p.stdin.on("error",()=>{});let x="";p.stdout.on("data",I=>{x+=I;let _;for(;(_=x.indexOf(`
24
+ `))>=0;){let A=x.slice(0,_);if(x=x.slice(_+1),!A.trim())continue;let F;try{F=JSON.parse(A)}catch{continue}process.env.LOOP_DEBUG&&console.log(Date.now()%1e6,F.type,F.subtype||F.event?.type||"",F.event?.content_block?.type||F.event?.delta?.type||"");try{Fn(g,e,F)}catch(C){console.error(C)}}}),p.stderr.on("data",I=>{g.stderr=(g.stderr+I).slice(-2e3)}),p.on("error",I=>{g.stderr+=I.message}),p.on("close",I=>{g.dead=!0,clearTimeout(g.idle),B.get(e.id)===g&&B.delete(e.id);let _=g.turn;g.turn=null;let A=g.resuming&&!g.turns;if(!_){A&&(e.sessionId=null);return}let F=_.c,C=S.get(e.id);if(C?.commentId===F.id){if(!C.canceled&&A)return e.sessionId=null,S.delete(e.id),Nt(F,!0);C.canceled&&!C.steered&&!g.turns&&!g.resuming&&(e.sessionId=null),V(F,"error",String(_.lastText&&C.canceled?_.lastText:g.stderr.trim()||`Agent exited with code ${I}`).slice(-400))}}),Rt(g);let j=[...B.values()].filter(I=>!I.turn&&I!==g);for(;B.size>_n&&j.length;)pe(j.shift());return g}var En=()=>(d.agent.allow||[]).filter(e=>!e.startsWith("dir:")),Cn=()=>(d.agent.allow||[]).filter(e=>e.startsWith("dir:")).map(e=>e.slice(4)),Dn=/^(cd|tail|head|grep|sort|uniq|wc|cat|echo|true|sleep|tee|xargs)$/;function Mn(e){let t=String(e.tool_name||""),o=e.tool_input||{};if(t==="Bash"){let r=String(o.command||"").split(/&&|\|\||;|\|/).map(p=>p.trim()).filter(Boolean),s=r.find(p=>!Dn.test(p.split(/\s+/)[0]))||r[0];if(!s)return null;let a=[];for(let p of s.split(/\s+/)){if(a.length===3||/^\d*[<>]|[<>&|$`'"*(){};]/.test(p))break;a.push(p)}if(!a.length)return null;let l=a.join(" ");return{tool:t,label:l,rule:`Bash(${l}:*)`,cmd:String(o.command).slice(0,300)}}let i=o.file_path||o.notebook_path||o.path;if(/^(Read|Edit|MultiEdit|Write|NotebookEdit|Glob|Grep)$/.test(t)&&i){let r=m.resolve(String(i)),s=/^(Glob|Grep)$/.test(t)?r:m.dirname(r);return{tool:t,label:`${/^(Read|Glob|Grep)$/.test(t)?"Read":"Edit"} files in ${s.replace(ae.homedir(),"~")}`,rule:`dir:${s}`,cmd:String(i)}}return t?{tool:t,label:t.replace(/^mcp__/,"").replace(/__/g," \xB7 "),rule:t}:null}function Ln(e){let t=[],o=new Set(d.agent.allow||[]);for(let i of e||[]){let r=Mn(i);r&&!o.has(r.rule)&&!t.some(s=>s.rule===r.rule)&&t.push(r)}return t}function Fn(e,t,o){o.session_id&&(t.sessionId=o.session_id);let i=e.turn;if(!i)return;let r=i.c,s=o.type==="system"&&o.model||o.type==="assistant"&&o.message?.model;if(s&&r.model!==s&&(r.model=s,d.agent.lastModel=s,k()),r.t&&r.t.first==null&&o.type==="stream_event"&&(r.t.first=Date.now()-r.createdAt),r.t&&r.t.tool==null&&o.type==="stream_event"&&o.event?.content_block?.type==="tool_use"&&(r.t.tool=Date.now()-r.createdAt),o.type==="stream_event"){let a=o.event||{};if(a.type==="message_start"&&S.get(t.id)?.step==="Reading your comment\u2026"&&J(t.id,"Thinking\u2026"),a.type==="content_block_start"){let l=a.content_block||{};l.type==="thinking"?J(t.id,"Thinking\u2026"):l.type==="tool_use"?(i.partial="",J(t.id,gt(l.name,{}))):l.type==="text"&&(i.partial="")}else a.type==="content_block_delta"&&a.delta?.type==="text_delta"&&(i.partial+=a.delta.text,i.flush||(i.flush=setTimeout(()=>{i.flush=null,e.turn===i&&J(t.id,"Writing\u2026",i.partial.slice(-4e3))},90)))}else if(o.type==="assistant"){for(let a of o.message?.content||[])if(a.type==="tool_use")J(t.id,gt(a.name,a.input));else if(a.type==="text"&&a.text.trim()){i.lastText=a.text.trim();let l=r.steps||=[];l.push({t:Date.now(),s:i.lastText.slice(0,600),say:!0}),l.length>60&&l.splice(0,l.length-60),i.partial="",k()}}else if(o.type==="result"){clearTimeout(i.flush),e.turn=null,e.turns++,Rt(e);let a=o.result??i.lastText,l=r.steps?.[r.steps.length-1];l?.say&&String(a||"").trim().startsWith(l.s.slice(0,80))&&r.steps.pop();let p=!!o.is_error||(o.permission_denials||[]).some(x=>/^(Edit|MultiEdit|Write)$/.test(x.tool_name)),g=Ln(o.permission_denials);if(g.length?r.blocked=g:delete r.blocked,e.restart&&pe(e),S.get(t.id)?.commentId!==r.id)return;a!=null&&!p?V(r,"done",String(a).trim().slice(0,4e3)||"Done."):V(r,"error",String(a||e.stderr.trim()||"Something went wrong.").slice(-400))}}function Rn(e){if(!je||xe()!=="claude"||S.has(e.id)||d.agent.mode==="inbox"&&Re())return!1;try{return Ze(e),!0}catch{return!1}}async function Nt(e,t=!1){let o=H(e.frameId);if(!o)return;if(xe()==="codex")return Wt(e,o,t);if(!je)return S.set(o.id,{commentId:e.id,step:"",startedAt:Date.now()}),V(e,"error",'No agent found to run this. Install Claude Code or Codex, or switch to "Your agent session" in the top-right menu.');let i={commentId:e.id,step:"Reading your comment\u2026",startedAt:e.startedAt&&t?e.startedAt:Date.now()};S.set(o.id,i),e.startedAt=i.startedAt,e.status="working",k();let r,s=!!B.get(o.id)&&!B.get(o.id).dead;try{r=Ze(o,t)}catch(p){return V(e,"error",`Couldn't start the agent: ${p.message}`)}r.warmed=s,i.child=r.child;let a=!t&&!e.snapshot?await rt(o,e.id):e.snapshot;if(e.snapshot=a,S.get(o.id)!==i)return;if(i.canceled)return V(e,"canceled","Stopped.");if(r.dead){try{r=Ze(o,!0)}catch(p){return V(e,"error",`Couldn't start the agent: ${p.message}`)}i.child=r.child}let l=e.kind==="create"||o.kind==="canvas"?at(o,e):Ct(o,e).message;r.turn={c:e,lastText:"",partial:"",flush:null},clearTimeout(r.idle),e.t={sent:Date.now()-e.createdAt,warm:!!r.warmed},r.child.stdin.write(JSON.stringify({type:"user",message:{role:"user",content:[{type:"text",text:l}]}})+`
25
+ `)}async function Wt(e,t,o=!1){S.set(t.id,{commentId:e.id,step:"Starting Codex\u2026",startedAt:Date.now()}),e.startedAt=Date.now(),e.status="working",k(),!o&&!e.snapshot&&(e.snapshot=await rt(t,e.id));let i=at(t,e),r=t.kind==="folder"?t.src:le,{model:s,effort:a}=ve("codex"),l=["--json","-c","sandbox_workspace_write.network_access=true",...s?["-m",s]:[],...a?["-c",`model_reasoning_effort="${a}"`]:[]],p=!!(t.codexSession&&!o&&e.kind!=="create"),g=p?["exec","resume",...l,"--skip-git-repo-check","-c",'sandbox_mode="workspace-write"',"-c",`sandbox_workspace_write.writable_roots=${JSON.stringify([r,O])}`,t.codexSession,"-"]:["exec",...l,"-s","workspace-write","--skip-git-repo-check","-C",r,"--add-dir",O,"-"];s&&(e.model=s,d.agent.lastModel=s,k());let x;try{x=Oe(tt,g,{cwd:r,env:process.env,stdio:["pipe","pipe","pipe"]})}catch(C){return V(e,"error",`Couldn't start Codex: ${C.message}`)}S.get(t.id).child=x,x.stdin.on("error",()=>{}),x.stdin.end(i);let j="",I="",_=null,A=!1,F="";x.stdout.on("data",C=>{j+=C;let D;for(;(D=j.indexOf(`
26
+ `))>=0;){let R=j.slice(0,D);j=j.slice(D+1);let W;try{W=JSON.parse(R)}catch{continue}let Y=W.item||{};W.type==="thread.started"&&W.thread_id&&e.kind!=="create"?t.codexSession=W.thread_id:Y.type==="command_execution"&&W.type==="item.started"?J(t.id,/\bcat |sed -n|rg |ls\b|grep /.test(Y.command||"")?"Reading the code":"Running a command"):Y.type==="file_change"?J(t.id,`Editing ${m.basename(Y.changes?.[0]?.path||"")}`.trim()):Y.type==="agent_message"&&Y.text?.trim()?(I=Y.text.trim(),J(t.id,"Thinking\u2026")):Y.type==="reasoning"?J(t.id,"Thinking\u2026"):W.type==="turn.completed"?A=!0:(W.type==="turn.failed"||W.type==="error")&&(_=W.error?.message||W.message||"Codex failed")}}),x.stderr.on("data",C=>{F=(F+C).slice(-2e3)}),x.on("error",C=>{F+=C.message}),x.on("close",C=>{if(!S.get(t.id)?.canceled&&!A&&p)return t.codexSession=null,S.delete(t.id),Wt(e,t,!0);A&&!_?V(e,"done",I.slice(0,4e3)||"Done."):V(e,"error",String(_||F.trim()||`Codex exited with code ${C}`).slice(-400))})}function Nn(){for(let e of Object.values(ae.networkInterfaces()))for(let t of e||[])if(t.family==="IPv4"&&!t.internal)return t.address;return null}var K=0;function Ve(){let e=Nn();return{lan:e?`http://${e}:${K}${re}/?t=${d.token}`:null,public:ne?`${ne}${re}/?t=${d.token}`:null,tunnelAvailable:!!me("cloudflared")}}function Wn(){return new Promise((e,t)=>{if(ne)return e(ne);let o=me("cloudflared");if(!o)return t(new Error("Public links need cloudflared. Install it with: brew install cloudflared"));fe=Oe(o,["tunnel","--no-autoupdate","--url",`http://localhost:${K}`],{stdio:["ignore","pipe","pipe"]});let i=r=>{let s=String(r).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);s&&!ne&&(ne=s[0],k(),e(ne))};fe.stdout.on("data",i),fe.stderr.on("data",i),fe.on("close",()=>{ne=null,fe=null,k()}),setTimeout(()=>t(new Error("The tunnel took too long to start")),2e4)})}var Gn={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".woff2":"font/woff2",".woff":"font/woff",".ttf":"font/ttf",".ico":"image/x-icon",".mp4":"video/mp4",".txt":"text/plain; charset=utf-8"};function Gt(e,t){let o=e.socket.remoteAddress||"",i=o==="127.0.0.1"||o==="::1"||o==="::ffff:127.0.0.1",r=e.headers["x-forwarded-for"]||e.headers["cf-connecting-ip"];if(i&&!r)return"owner";let s=/(?:^|;\s*)loop_t=([^;]+)/.exec(e.headers.cookie||"")?.[1];return t.searchParams.get("t")===d.token||s===d.token?"guest":null}async function L(e){let t=[],o=0;for await(let r of e){if(o+=r.length,o>15e6)throw new Error("too large");t.push(r)}let i=Buffer.concat(t).toString("utf8");return i?JSON.parse(i):{}}function u(e,t,o){e.writeHead(t,{"content-type":"application/json","cache-control":"no-store"}),e.end(JSON.stringify(o))}async function te(e,t,o={}){try{if((await P.stat(t)).isDirectory())return te(e,m.join(t,"index.html"),o);e.writeHead(200,{"content-type":Gn[m.extname(t).toLowerCase()]||"application/octet-stream","cache-control":"no-store",...o}),$.createReadStream(t).pipe(e)}catch{e.writeHead(404),e.end("Not found")}}var oe=(e,t)=>t===e||t.startsWith(e+m.sep),Ce=vt.createServer(async(e,t)=>{let o=new URL(e.url,"http://x"),i;try{i=decodeURIComponent(o.pathname)}catch{return t.writeHead(400),t.end()}let r=Gt(e,o);if(!r)return t.writeHead(403,{"content-type":"text/plain"}),t.end("This Loop canvas is private. Ask the host for a share link.");let s=r==="owner";if(i!==re&&!i.startsWith(re+"/"))return d.proxy?an(e,t,d.proxy):e.headers["sec-fetch-dest"]==="iframe"?ut(t,"No app is connected to this canvas yet","Run <code>loop scan</code> in the project, or give Loop the running app with <code>loop scan --url http://localhost:&lt;port&gt;</code>."):i==="/"?(t.writeHead(302,{location:`${re}/${o.search}`}),t.end()):(t.writeHead(404),t.end("Not found"));if(i=i.slice(re.length)||"/",i==="/"&&e.headers["sec-fetch-dest"]==="iframe")return ut(t,"A frame can\u2019t show the canvas itself","This frame points at Loop\u2019s own address. Point it at your app instead.");try{if(i==="/"||i==="/index.html"){let n=o.searchParams.get("t")?{"set-cookie":`loop_t=${d.token}; Path=/; HttpOnly; SameSite=Lax`}:{};return te(t,m.join(Ge,"index.html"),n)}if(i.startsWith("/app/")){let n=m.join(Ge,m.normalize(i.slice(5)));return oe(Ge,n)?te(t,n):(t.writeHead(400),t.end())}let a=/^\/optview\/([A-Za-z0-9_-]+)\/([A-F])\/(.*)$/.exec(i);if(a){let n=d.comments.find(v=>v.id===a[1]),c=n&&H(n.frameId),f=n?.options?.find(v=>v.id===a[2]);if(!c||!f||c.kind!=="folder")return t.writeHead(404),t.end();let h=m.normalize(decodeURIComponent(a[3]||c.entry)),w=m.join(f.dir,h),y=m.join(c.src,h);return!oe(f.dir,w)||!oe(c.src,y)?(t.writeHead(400),t.end()):te(t,$.existsSync(w)?w:y)}let l=/^\/hist\/([A-Za-z0-9_-]+)\/(.*)$/.exec(i);if(l){let n=m.join(O,"history",l[1]),c=m.join(n,m.normalize(l[2]||"index.html"));return oe(n,c)?te(t,c):(t.writeHead(400),t.end())}let p=/^\/poster\/([A-Za-z0-9_-]+)$/.exec(i);if(p){let n=d.frames.find(c=>c.id===p[1]);if(!n)return t.writeHead(404),t.end();try{return te(t,await xn(n),{"cache-control":"private, max-age=3600"})}catch{return t.writeHead(404),t.end()}}let g=/^\/shots\/([\w.-]+\.png)$/.exec(i);if(g)return te(t,m.join(O,"shots",g[1]));let x=/^\/attachments\/([A-Za-z0-9_-]+)\/([^/]+)$/.exec(i);if(x){let n=m.join(O,"attachments",x[1]),c=m.join(n,decodeURIComponent(x[2]));return oe(n,c)?te(t,c):(t.writeHead(400),t.end())}let j=/^\/f\/([^/]+)\/(.*)$/.exec(i);if(j){let n=d.frames.find(f=>f.id===j[1]);if(!n||n.kind!=="folder"&&n.kind!=="frozen")return t.writeHead(404),t.end();let c=m.join(n.src,m.normalize(j[2]||n.entry));return oe(n.src,c)?te(t,c):(t.writeHead(400),t.end())}if(i==="/api/ping")return u(t,200,{ok:!0,name:d.name,pid:process.pid});if(i==="/api/state")return u(t,200,jt(r));if(i==="/api/events"){t.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-store",connection:"keep-alive"});let n={res:t,role:r};De.add(n),Ue(t,_t(n));let c=setInterval(()=>t.write(`: ka
27
27
 
28
- `),2e4);e.on("close",()=>{clearInterval(c),ze.delete(n)});return}if(e.method==="POST"&&i==="/api/presence"){let n=await D(e);return Z({type:"presence",id:String(n.id).slice(0,32),name:String(n.name||"Guest").slice(0,32),color:String(n.color||"#888").slice(0,16),x:+n.x,y:+n.y,leave:!!n.leave}),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/comments"){let n=await D(e),c=z(n.frameId);if(!c||!String(n.text||"").trim())return u(t,400,{error:"frame and text required"});let f=typeof n.id=="string"&&/^[A-Za-z0-9_-]{8}$/.test(n.id)&&!d.comments.some(S=>S.id===n.id)?n.id:R(8),h=null;typeof n.annotationPng=="string"&&n.annotationPng.startsWith("data:image/png;base64,")&&(await _.mkdir(m.join(O,"annotations"),{recursive:!0}),h=m.join(O,"annotations",`${f}.png`),await _.writeFile(h,Buffer.from(n.annotationPng.split(",")[1],"base64")));let g=[];for(let S of(Array.isArray(n.attachments)?n.attachments:[]).slice(0,6)){let P=/^data:([\w.+-]+\/[\w.+-]+)?(;[^,]*)?;base64,(.*)$/s.exec(String(S?.data||""));if(!P)continue;let Q=String(S.name||"file").replace(/[^\w.\- ]+/g,"_").replace(/^\.+/,"").slice(-80)||"file",T=m.join(O,"attachments",f);await _.mkdir(T,{recursive:!0});let ee=Q,Wt=1;for(;g.some(Gt=>Gt.name===ee);)ee=`${Wt++}-${Q}`;await _.writeFile(m.join(T,ee),Buffer.from(P[3],"base64")),g.push({name:ee,type:P[1]||"application/octet-stream",path:m.join(T,ee),url:`/__loop/attachments/${f}/${encodeURIComponent(ee)}`})}let y=n.parentId?d.comments.find(S=>S.id===n.parentId):null,v={id:f,frameId:c.id,parentId:y?y.parentId||y.id:null,text:String(n.text).slice(0,4e3),anchor:n.anchor||y?.anchor||null,ink:Array.isArray(n.ink)?n.ink.slice(0,20):[],inkStrokes:Array.isArray(n.inkStrokes)?n.inkStrokes.map(String):[],annotation:h,author:{name:String(n.author?.name||(s?"You":"Guest")).slice(0,32),color:String(n.author?.color||"#2F5BFF").slice(0,16),guest:!s},status:s?"queued":"waiting",reply:null,createdAt:Date.now(),...g.length?{attachments:g}:{}};d.comments.push(v);for(let S of d.strokes)v.inkStrokes.includes(S.id)&&(S.commentId=v.id);if(s&&ln(v,c),k(),s&&c.kind==="folder"&&v.kind!=="create"){let S=bn(v.text);for(let P=0;P<S;P++)try{await kt(v,c)}catch{break}}return ie(c.id),u(t,200,v)}let I=/^\/api\/comments\/([^/]+)(?:\/(\w+))?$/.exec(i);if(I){let n=d.comments.find(f=>f.id===I[1]);if(!n)return u(t,404,{error:"no such comment"});let c=I[2];if(e.method==="DELETE"&&!c){if(!s&&!n.author?.guest)return u(t,403,{});let f=new Set([n.id,...d.comments.filter(g=>g.parentId===n.id).map(g=>g.id)]),h=x.get(n.frameId);return h&&f.has(h.commentId)&&(h.canceled=!0,h.child?h.child.kill("SIGTERM"):x.delete(n.frameId)),d.comments=d.comments.filter(g=>!f.has(g.id)),d.strokes=d.strokes.filter(g=>!f.has(g.commentId)),k(),u(t,200,{ok:!0})}if(!s)return u(t,403,{error:"host only"});if(c==="send")return["waiting","error","canceled"].includes(n.status)&&(n.status="queued",n.snapshot=null,n.undone=!1,n.redoSnap=null,k(),ie(n.frameId)),u(t,200,n);if(c==="background")return s?n.status!=="queued"?u(t,409,{error:"It already started"}):(n.background=!0,k(),ie(n.frameId),u(t,200,{ok:!0})):u(t,403,{});if(c==="steer")return s?pn(n)?u(t,200,{ok:!0}):u(t,409,{error:"Nothing to steer into: the run already finished"}):u(t,403,{});if(c==="cancel"){let f=x.get(n.frameId);return f?.commentId===n.id?(f.canceled=!0,f.child?f.child.kill("SIGTERM"):Y(n,"canceled","Stopped.")):n.status==="queued"&&(n.status="canceled",k()),u(t,200,{ok:!0})}if(c==="undo"){let f=z(n.frameId);if(!n.snapshot||!f||f.kind!=="folder")return u(t,400,{error:"nothing to undo"});if(x.has(f.id))return u(t,409,{error:"The agent is still working on this frame \u2014 wait for it to finish (or stop it) first."});let h=d.comments.filter(y=>y.id!==n.id&&y.frameId===n.frameId&&y.snapshot&&y.status==="done"&&!y.undone&&(y.startedAt||y.createdAt)>(n.startedAt||n.createdAt)),g=await D(e);if(h.length&&!g.force)return u(t,409,{error:"newer",newer:h.length});n.redoSnap=await je(f,`${n.id}-after`),await Ae(f,n.snapshot),n.undone=!0;for(let y of h)y.undone=!0;return k(),Z({type:"frame-changed",frameId:f.id}),u(t,200,{ok:!0,reverted:1+h.length})}if(c==="redo"){let f=z(n.frameId);return!n.redoSnap||!n.undone||!f?u(t,400,{error:"nothing to redo"}):x.has(f.id)?u(t,409,{error:"The agent is still working on this frame \u2014 wait for it to finish first."}):(await Ae(f,n.redoSnap),n.undone=!1,k(),Z({type:"frame-changed",frameId:f.id}),u(t,200,{ok:!0}))}if(c==="resolve")return n.resolved=!n.resolved,k(),u(t,200,n);if(c==="allow"&&e.method==="POST"){let f=await D(e),h=n.blocked?.[+f.i];if(!h||h.allowed)return u(t,400,{error:"Nothing to allow"});let g=d.agent.allow||=[];g.includes(h.rule)||g.push(h.rule),h.allowed=!0;let y=U.get(n.frameId);y&&!y.turn?pe(y):y&&(y.restart=!0);let v=d.comments.find(P=>P.id===(n.parentId||n.id)),S=h.tool==="Bash"?`\`${h.label}\``:h.label.charAt(0).toLowerCase()+h.label.slice(1);return d.comments.push({id:R(8),frameId:n.frameId,parentId:v?.id||n.id,anchor:n.anchor,ink:[],inkStrokes:[],text:`Allowed ${S}. Go ahead and use it, then carry on with what you were doing.`,author:v?.author||{name:"You",color:"#2F5BFF"},status:"queued",reply:null,createdAt:Date.now()}),k(),ie(n.frameId),u(t,200,{ok:!0})}if(c==="note"){let f=await D(e),h=String(f.text||"").trim().slice(0,2e3);if(!h)return u(t,400,{error:"text required"});let g={id:R(8),kind:"note",frameId:n.frameId,parentId:n.parentId||n.id,text:h,anchor:n.anchor,author:{name:fe[be()].label,agent:!0,color:"#17171A"},model:f.model?String(f.model).slice(0,60):void 0,status:"done",createdAt:Date.now()};return d.comments.push(g),k(),Z({type:"frame-changed",frameId:n.frameId}),u(t,200,g)}if(c==="step"){let f=await D(e);return B(n.frameId,String(f.text||"").slice(0,120)),u(t,200,{ok:!0})}if(c==="option"&&e.method==="POST"){let f=await D(e),h=z(n.frameId);if(!h)return u(t,404,{error:"no frame"});if(n.options=n.options||[],h.kind==="folder"){let v=String(f.name||"").trim().slice(0,60),S=n.options.find(P=>P.placeholder&&P.frameId);try{if(S){S.placeholder=!1,S.name=v||S.name;let P=d.frames.find(Q=>Q.id===S.frameId);P&&v&&(P.name=he(`${S.id} \xB7 ${v}`)),S.madeAt=Date.now(),k()}else S=await kt(n,h,v||`Option ${String.fromCharCode(65+n.options.length)}`)}catch(P){return u(t,400,{error:P.message})}return u(t,200,{id:S.id,dir:S.dir,root:h.src,frame:!0})}if(n.options.length>=6)return u(t,400,{error:"up to 6 options"});let g=String.fromCharCode(65+n.options.length),y=m.join(O,"options",n.id,g);return await _.rm(y,{recursive:!0,force:!0}),await _.mkdir(y,{recursive:!0}),n.options.push({id:g,name:String(f.name||`Option ${g}`).slice(0,60),dir:y}),k(),u(t,200,{id:g,dir:y,root:Et(h)})}if(c==="thumbs"&&e.method==="POST")return Dt(n),u(t,200,{ok:!0});if(c==="preview"&&e.method==="POST"){if(!s)return u(t,403,{});let f=await D(e);try{await gt(n,f.option||null)}catch(h){return u(t,400,{error:h.message})}return u(t,200,{ok:!0})}if(c==="clearoptions"&&e.method==="POST"){let f=new Set((n.options||[]).map(h=>h.frameId).filter(Boolean));for(let h of f){let g=x.get(h);g&&(g.canceled=!0,g.child?.kill("SIGTERM"),x.delete(h)),_e(h)}d.frames=d.frames.filter(h=>!f.has(h.id)),d.comments=d.comments.filter(h=>!f.has(h.frameId)),d.sections=d.sections.filter(h=>h.id!==n.optionSection);for(let h of n.options||[])delete h.frameId;return n.optionsCleared=!0,delete n.optionSection,k(),u(t,200,{ok:!0})}if(c==="choose"&&e.method==="POST"){if(!s)return u(t,403,{});let f=await D(e),h=n.options?.find(y=>y.id===f.option&&y.frameId),g=h&&d.frames.find(y=>y.id===h.frameId);if(g){let y=z(n.frameId);if(!y||y.kind!=="folder")return u(t,404,{error:"The original frame is gone."});if(x.has(y.id))return u(t,409,{error:"The agent is still working on the original frame. Wait for it to finish (or stop it) first."});let v=`r${R(7)}`;return await je(y,v)?((y.restores||=[]).push({id:v,at:Date.now()}),await Ae(y,g.src),n.optionChosen=h.id,ae.set(y.id,(ae.get(y.id)||0)+1),k(),Z({type:"frame-changed",frameId:y.id}),u(t,200,{ok:!0})):u(t,500,{error:"Could not keep the current version, so nothing was changed."})}try{await gt(n,f.option)}catch(y){return u(t,400,{error:y.message})}return n.optionChosen=f.option,k(),await _.rm(m.join(O,"options",n.id,"_original"),{recursive:!0,force:!0}).catch(()=>{}),u(t,200,{ok:!0})}if(c==="reply"){let f=await D(e);return x.has(n.frameId)||x.set(n.frameId,{commentId:n.id,step:"",startedAt:Date.now()}),f.model&&(n.model=String(f.model).slice(0,60),d.agent.lastModel=n.model),n.startedAt||(n.startedAt=x.get(n.frameId)?.startedAt),Y(n,f.error?"error":"done",String(f.text||"Done.").slice(0,4e3)),u(t,200,n)}}if(!s)return u(t,403,{error:"host only"});if(i==="/api/inbox"){let n=Date.now()+(o.searchParams.get("wait")?25e3:0),c=Le(),f=!1;for(e.on("close",()=>{f=!0}),Ke=Date.now(),c||k(!0);;){if(f)return;Ke=Date.now();let h=d.comments.find(g=>g.status==="queued"&&!g.background&&!x.has(g.frameId));if(h){let g=z(h.frameId),y={commentId:h.id,step:"Your chat picked this up",startedAt:Date.now(),inbox:!0};return x.set(g.id,y),setTimeout(()=>{x.get(g.id)===y&&y.step==="Your chat picked this up"&&B(g.id,"Working\u2026")},3e3),h.status="working",k(),[h.snapshot]=await Promise.all([h.snapshot||it(g,h.id),un(h)]),u(t,200,{comment:{id:h.id,kind:h.kind||"comment",text:h.text,anchor:h.anchor,ink:h.ink,annotation:h.annotation,attachments:h.attachments},frame:{id:g.id,name:g.name,kind:g.kind,src:g.src,entry:g.entry,w:g.w,h:g.h},prompt:st(g,h)})}if(Date.now()>n)return u(t,200,{comment:null});await new Promise(g=>{let y=()=>{clearTimeout(v),Ue.delete(y),g()},v=setTimeout(y,2e3);Ue.add(y)})}}if(e.method==="POST"&&i==="/api/frames"){let n=await D(e);if(n.prompt){let c=n.name||n.prompt.split(/[.\n]/)[0].slice(0,40),f=ut(c),h=q({src:f,name:c,w:n.w,h:n.h,x:n.x,y:n.y});return d.comments.push({id:R(8),kind:"create",frameId:h.id,text:n.prompt,author:{name:"You",color:"#2F5BFF"},status:"queued",createdAt:Date.now()}),k(),ie(h.id),u(t,200,{...h,dir:f})}if(n.new){let c=ut(n.name||"Untitled",!!n.blank);typeof n.html=="string"&&n.html.length<4e6&&$.writeFileSync(m.join(c,"index.html"),n.html);let f=n.x==null&&Sn();if(f)return u(t,200,{...xn(f.c,f.f,c,n.name),dir:c});let h=q({...n,src:c});return n.parent&&d.frames.some(g=>g.id===n.parent&&g.id!==h.id)&&(h.parent=n.parent,k()),(n.fill==="none"||/^#[0-9a-f]{6}$/i.test(n.fill||""))&&(h.fill=n.fill.toLowerCase(),k()),u(t,200,{...h,dir:c})}return n.route!=null?u(t,200,q(n)):n.src?!/^https?:\/\//.test(n.src)&&!$.existsSync(m.resolve(de,n.src))?u(t,400,{error:`No such file or folder: ${n.src}`}):u(t,200,q(n)):u(t,400,{error:"src, route, new or prompt required"})}let A=/^\/api\/frames\/([^/]+)\/versions(\/restore|\/duplicate)?$/.exec(i);if(A&&e.method==="POST"){let n=d.frames.find(S=>S.id===A[1]);if(!n||n.kind!=="folder")return u(t,400,{error:"Only static frames keep versions."});let c=encodeURIComponent(n.entry||"index.html");if(!A[2]){let S=[...d.comments.filter(T=>T.frameId===n.id&&T.snapshot&&!T.snapThumb&&$.existsSync(T.snapshot)),...(n.restores||[]).filter(T=>!T.thumb)].slice(-14),P=async T=>{try{let ee=await ce(n,`http://127.0.0.1:${X}/__loop/hist/${T.id}/${c}`);T["frameId"in T?"snapThumb":"thumb"]=`/__loop/shots/${m.basename(ee)}`,k()}catch{}},[Q]=await Promise.all([ce(n).catch(()=>null),...S.map(P)]);return u(t,200,{now:Q?`/__loop/shots/${m.basename(Q)}`:null})}let f=await D(e),h=String(f.id||"");if(!/^[A-Za-z0-9_-]+$/.test(h))return u(t,400,{error:"no such version"});let g=d.comments.some(S=>S.id===h&&S.frameId===n.id&&S.snapshot)||(n.restores||[]).some(S=>S.id===h),y=m.join(O,"history",h);if(!g||!$.existsSync(y))return u(t,404,{error:"That version is no longer kept."});if(A[2]==="/duplicate"){let S=he(String(f.name||`${n.name} \xB7 earlier version`).slice(0,80)),P=ge(S);return await re(y,P),u(t,200,q({src:m.join(P,n.entry||"index.html"),name:S,w:n.w,h:n.h,near:n.id}))}if(x.has(n.id))return u(t,409,{error:"The agent is still working on this frame. Wait for it to finish (or stop it) first."});let v=`r${R(7)}`;return await je(n,v)?((n.restores||=[]).push({id:v,at:Date.now()}),await Ae(n,y),ae.set(n.id,(ae.get(n.id)||0)+1),k(),Z({type:"frame-changed",frameId:n.id}),u(t,200,{ok:!0})):u(t,500,{error:"Could not keep the current version, so nothing was changed."})}let M=/^\/api\/frames\/([^/]+)\/(source|edit)$/.exec(i);if(M){let n=d.frames.find(h=>h.id===M[1]);if(!n||!s)return u(t,404,{error:"no such frame"});if(n.kind!=="folder")return u(t,400,{error:"Only static frames can be edited directly."});let c=m.join(n.src,n.entry||"index.html"),f;try{f=await _.readFile(c,"utf8")}catch{return u(t,404,{error:"The frame has no page to edit."})}if(M[2]==="source"&&e.method==="GET")return u(t,200,{text:f});if(M[2]==="edit"&&e.method==="POST"){if(x.has(n.id))return u(t,409,{error:"The agent is working on this frame. Try again when it is done."});let h=await D(e),g=(Array.isArray(h.edits)?h.edits:[]).map(T=>({a:T.a|0,b:T.b|0,before:String(T.before??""),text:String(T.text??"")})).sort((T,ee)=>T.a-ee.a);if(!g.length||g.length>40)return u(t,400,{error:"nothing to change"});let y=0;for(let T of g){if(T.a<y||T.b<T.a||T.b>f.length||f.slice(T.a,T.b)!==T.before)return u(t,409,{error:"The page changed since you started editing. Try again."});y=T.b}let v=ht.get(n.id)||0,S=Date.now();if(S-v>9e4){let T=`e${R(7)}`;await je(n,T)&&(n.restores||=[]).push({id:T,at:S,what:String(h.what||"Edited by hand").slice(0,80)})}ht.set(n.id,S);let P="",Q=0;for(let T of g)P+=f.slice(Q,T.a)+T.text,Q=T.b;return P+=f.slice(Q),await _.writeFile(c,P),k(),u(t,200,{ok:!0})}}let L=/^\/api\/frames\/([^/]+)\/warm$/.exec(i);if(L&&e.method==="POST"){let n=z(L[1]);return Fe&&n?.kind!=="canvas"&&Ct().catch(()=>{}),u(t,200,{warm:!!(n&&Dn(n))})}let E=/^\/api\/frames\/([^/]+)\/shot$/.exec(i);if(E&&e.method==="POST"){let n=d.frames.find(h=>h.id===E[1]);if(!n||!s)return u(t,404,{error:"no such frame"});let c=await D(e),f;if(c.option){let[h,g]=String(c.option).split(":"),y=d.comments.find(S=>S.id===h);if(!y?.options?.some(S=>S.id===g))return u(t,400,{error:"no such option"});let v=d.frames.find(S=>S.id===y.options.find(P=>P.id===g)?.frameId);if(v)try{return u(t,200,{path:await ce(v)})}catch(S){return u(t,500,{error:S.message})}if(n.kind!=="folder")return u(t,400,{error:"Options on app screens can't be screenshotted before they're applied. Describe them clearly instead."});f=`http://127.0.0.1:${X}/__loop/optview/${h}/${g}/${encodeURIComponent(n.entry||"index.html")}`}x.get(n.id)?.inbox&&B(n.id,"Looking at the screen");try{return u(t,200,{path:await ce(n,f)})}catch(h){return u(t,500,{error:h.message})}}let C=/^\/api\/frames\/([^/]+)\/freeze$/.exec(i);if(C&&e.method==="POST"){let n=d.frames.find(v=>v.id===C[1]);if(!n)return u(t,404,{error:"no such frame"});let c=await D(e);if(typeof c.html!="string"||!c.html)return u(t,400,{error:"nothing captured"});let f=he(`${n.name.replace(/ · frozen( \d+)?$/,"")} \xB7 frozen`),h=ge(f);await _.mkdir(h,{recursive:!0}),await _.writeFile(m.join(h,"index.html"),c.html);let g=typeof c.from=="string"&&c.from.startsWith("/")&&!c.from.startsWith("/__loop/")?c.from.slice(0,2e3):n.kind==="frozen"?n.from:n.kind==="route"?n.src:void 0,y={id:R(6),name:f,kind:"frozen",src:h,entry:"index.html",from:g,frozenAt:Date.now(),w:n.w,h:n.h,keyboard:n.keyboard,...c.x!=null&&c.y!=null?{x:+c.x,y:+c.y}:Be(n.w,n.h,n.id),createdAt:Date.now()};return d.frames.push(y),k(),u(t,200,y)}let F=/^\/api\/frames\/([^/]+)\/duplicate$/.exec(i);if(F&&e.method==="POST"){let n=d.frames.find(S=>S.id===F[1]);if(!n)return u(t,404,{error:"no such frame"});let c=await D(e),f=n.kind==="route"&&typeof c.route=="string"&&c.route.startsWith("/")&&!c.route.startsWith("/__loop/")?c.route.slice(0,2e3):null,h=f&&f!==n.src,g=he(h?sn(f):`${n.name.replace(/ copy( \d+)?$/,"")} copy`),y=c.x!=null&&c.y!=null?{x:c.x,y:c.y}:{near:n.id},v;if(n.kind==="folder"||n.kind==="frozen"){let S=ge(g);await re(n.src,S),v=q({src:m.join(S,n.entry),name:g,w:n.w,h:n.h,...y}),n.kind==="frozen"&&(_e(v.id),Object.assign(v,{kind:"frozen",from:n.from,frozenAt:n.frozenAt}),k())}else n.kind==="route"?v=q({route:f||n.src,name:g,w:n.w,h:n.h,dup:!0,...y}):v=q({src:n.src,name:g,w:n.w,h:n.h,...y});return u(t,200,v)}let N=/^\/api\/frames\/([^/]+)$/.exec(i);if(N){let n=d.frames.find(c=>c.id===N[1]);if(!n)return u(t,404,{});if(e.method==="PATCH"){let c=await D(e);for(let f of["x","y","w","h"])c[f]!=null&&isFinite(c[f])&&(n[f]=Math.round(+c[f]));return c.name&&(n.name=String(c.name).slice(0,80)),typeof c.keyboard=="boolean"&&(n.keyboard=c.keyboard||void 0),"fill"in c&&(n.fill=c.fill==="none"?"none":typeof c.fill=="string"&&/^#[0-9a-f]{6}$/i.test(c.fill)?c.fill.toLowerCase():void 0),"parent"in c&&(n.parent=c.parent&&c.parent!==n.id&&d.frames.some(f=>f.id===c.parent)?c.parent:void 0),k(),u(t,200,n)}if(e.method==="DELETE"){let c=x.get(n.id);return c&&(c.canceled=!0,c.child?.kill("SIGTERM"),x.delete(n.id)),d.frames=d.frames.filter(f=>f.id!==n.id),d.comments=d.comments.filter(f=>f.frameId!==n.id),_e(n.id),k(),u(t,200,{ok:!0})}}if(e.method==="POST"&&i==="/api/sections"){let n=await D(e);if(![n.x,n.y,n.w,n.h].every(Number.isFinite))return u(t,400,{error:"x, y, w and h required"});let c=d.sections.length+1;for(;d.sections.some(h=>h.name===`Section ${c}`);)c++;let f={id:R(6),name:String(n.name||`Section ${c}`).slice(0,80),x:Math.round(n.x),y:Math.round(n.y),w:Math.max(120,Math.round(n.w)),h:Math.max(120,Math.round(n.h)),createdAt:Date.now()};return d.sections.push(f),k(),u(t,200,f)}let J=/^\/api\/sections\/([^/]+)$/.exec(i);if(J){let n=d.sections.find(c=>c.id===J[1]);if(!n)return u(t,404,{});if(e.method==="PATCH"){let c=await D(e);for(let f of["x","y","w","h"])c[f]!=null&&isFinite(c[f])&&(n[f]=Math.round(+c[f]));return c.name&&(n.name=String(c.name).slice(0,80)),k(),u(t,200,n)}if(e.method==="DELETE")return d.sections=d.sections.filter(c=>c.id!==n.id),k(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/restore"){let n=await D(e);for(let c of n.sections||[])c?.id&&!d.sections.find(f=>f.id===c.id)&&d.sections.push(c);for(let c of n.frames||[])c?.id&&!d.frames.find(f=>f.id===c.id)&&(d.frames.push(c),nt(c));for(let c of n.strokes||[])c?.id&&!d.strokes.find(f=>f.id===c.id)&&d.strokes.push(c);for(let c of n.comments||[])c?.id&&!d.comments.find(f=>f.id===c.id)&&d.comments.push({...c,status:["working","queued"].includes(c.status)?"canceled":c.status});return k(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/strokes"){let n=await D(e),c={id:R(8),points:(n.points||[]).slice(0,6e3).map(h=>Math.round(h*10)/10),color:String(n.color||"#E5484D").slice(0,16),width:+n.width||3,frameId:n.frameId||null,createdAt:Date.now()},f=c.frameId&&d.frames.find(h=>h.id===c.frameId);return f&&(c.fx=Number.isFinite(+n.fx)?+n.fx:f.x,c.fy=Number.isFinite(+n.fy)?+n.fy:f.y),d.strokes.push(c),k(),u(t,200,c)}let Se=/^\/api\/strokes\/([^/]+)$/.exec(i);if(Se&&e.method==="DELETE")return d.strokes=d.strokes.filter(n=>n.id!==Se[1]),k(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/update")return!V||V.how!=="restart"?u(t,400,{error:"No update to apply here"}):V.state==="updating"?u(t,200,{ok:!0}):x.size?u(t,409,{error:"The agent is working. Update when it has finished."}):(Nn(),u(t,200,{ok:!0}));if(e.method==="POST"&&i==="/api/settings"){let n=await D(e);if((n.mode==="headless"||n.mode==="inbox")&&(d.agent.mode=n.mode),n.runner&&fe[n.runner]){if(!fe[n.runner].bin)return u(t,400,{error:`${fe[n.runner].label} isn't installed on this computer`});d.agent.runner=n.runner}if(n.proxy!==void 0){if(n.proxy&&!/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(n.proxy))return u(t,400,{error:"proxy must be a local dev server origin, e.g. http://localhost:3000"});d.proxy=n.proxy||null}if(n.chat&&typeof n.chat=="object"){let c=f=>typeof f=="string"&&/^[\w.:\-\[\]]{1,60}$/.test(f)?f:null;d.agent.chat={runner:n.chat.runner==="codex"?"codex":"claude",model:c(n.chat.model),effort:c(n.chat.effort)}}if(n.pick&&typeof n.pick=="object"){let c=be(),f=$e(c),h=(d.agent.pick||={})[c]||={};if("model"in n.pick){if(n.pick.model&&!f.models.includes(n.pick.model))return u(t,400,{error:"No such model"});h.model=n.pick.model||void 0}if("effort"in n.pick){if(n.pick.effort&&!$e(c).efforts.includes(n.pick.effort))return u(t,400,{error:"No such effort level"});h.effort=n.pick.effort||void 0}}if(n.disallow){d.agent.allow=(d.agent.allow||[]).filter(c=>c!==n.disallow);for(let c of[...U.values()])c.turn?c.restart=!0:pe(c)}return n.name&&(d.name=String(n.name).slice(0,80)),n.hostName!==void 0&&(d.hostName=String(n.hostName).trim().slice(0,32)||void 0),k(),at(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/share/public")try{return await Ln(),u(t,200,Ze())}catch(n){return u(t,400,{error:n.message})}if(e.method==="POST"&&i==="/api/share/stop")return le?.kill(),ne=null,k(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/share/rotate")return d.token=R(16),k(),u(t,200,Ze());if(i.startsWith("/api/cloud")&&!G)return u(t,404,{error:"Email sharing is not available in this version of Loop."});if(i==="/api/cloud"&&e.method==="GET")return u(t,200,await G.status());if(e.method==="POST"&&i==="/api/cloud/session"){let n=await D(e);return u(t,200,{user:await G.setSession(n)})}if(e.method==="POST"&&i==="/api/cloud/signout")return G.signOut(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/cloud/publish")return u(t,200,{link:await G.publish()});if(e.method==="POST"&&i==="/api/cloud/snapshot"){let n=await D(e),c=d.frames.find(h=>h.id===n.frameId);if(!c||c.kind!=="route"||typeof n.html!="string")return u(t,400,{error:"route frame and html required"});let f=await G.publishSnapshot(c,n.html);return G.schedulePush(200),u(t,200,f)}if(e.method==="POST"&&i==="/api/cloud/invite"){let n=await D(e);return u(t,200,await G.invite(n.emails||[],n.role))}if(e.method==="DELETE"&&i.startsWith("/api/cloud/members/"))return await G.removeMember(decodeURIComponent(i.slice(19))),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/shutdown")return u(t,200,{ok:!0}),setTimeout(Re,50);u(t,404,{error:"not found"})}catch(a){u(t,500,{error:a.message})}});Ee.on("upgrade",(e,t,o)=>{let i=new URL(e.url,"http://x");if(!d.proxy||i.pathname.startsWith(ue)||!Rt(e,i))return t.destroy();let r=new URL(d.proxy),s=Bt.connect(+(r.port||80),r.hostname,()=>{let a=`${e.method} ${e.url} HTTP/1.1\r
29
- `;for(let l=0;l<e.rawHeaders.length;l+=2){let p=e.rawHeaders[l],w=e.rawHeaders[l+1];p.toLowerCase()==="host"&&(w=r.host),a+=`${p}: ${w}\r
28
+ `),2e4);e.on("close",()=>{clearInterval(c),De.delete(n)});return}if(e.method==="POST"&&i==="/api/presence"){let n=await L(e);return Z({type:"presence",id:String(n.id).slice(0,32),name:String(n.name||"Guest").slice(0,32),color:String(n.color||"#888").slice(0,16),x:+n.x,y:+n.y,leave:!!n.leave}),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/comments"){let n=await L(e),c=H(n.frameId);if(!c||!String(n.text||"").trim())return u(t,400,{error:"frame and text required"});let f=typeof n.id=="string"&&/^[A-Za-z0-9_-]{8}$/.test(n.id)&&!d.comments.some(b=>b.id===n.id)?n.id:N(8),h=null;typeof n.annotationPng=="string"&&n.annotationPng.startsWith("data:image/png;base64,")&&(await P.mkdir(m.join(O,"annotations"),{recursive:!0}),h=m.join(O,"annotations",`${f}.png`),await P.writeFile(h,Buffer.from(n.annotationPng.split(",")[1],"base64")));let w=[];for(let b of(Array.isArray(n.attachments)?n.attachments:[]).slice(0,6)){let E=/^data:([\w.+-]+\/[\w.+-]+)?(;[^,]*)?;base64,(.*)$/s.exec(String(b?.data||""));if(!E)continue;let M=String(b.name||"file").replace(/[^\w.\- ]+/g,"_").replace(/^\.+/,"").slice(-80)||"file",T=m.join(O,"attachments",f);await P.mkdir(T,{recursive:!0});let ee=M,Ht=1;for(;w.some(Ut=>Ut.name===ee);)ee=`${Ht++}-${M}`;await P.writeFile(m.join(T,ee),Buffer.from(E[3],"base64")),w.push({name:ee,type:E[1]||"application/octet-stream",path:m.join(T,ee),url:`/__loop/attachments/${f}/${encodeURIComponent(ee)}`})}let y=n.parentId?d.comments.find(b=>b.id===n.parentId):null,v={id:f,frameId:c.id,parentId:y?y.parentId||y.id:null,text:String(n.text).slice(0,4e3),anchor:n.anchor||y?.anchor||null,ink:Array.isArray(n.ink)?n.ink.slice(0,20):[],inkStrokes:Array.isArray(n.inkStrokes)?n.inkStrokes.map(String):[],annotation:h,author:{name:String(n.author?.name||(s?"You":"Guest")).slice(0,32),color:String(n.author?.color||"#2F5BFF").slice(0,16),guest:!s},status:s?"queued":"waiting",reply:null,createdAt:Date.now(),...w.length?{attachments:w}:{}};d.comments.push(v);for(let b of d.strokes)v.inkStrokes.includes(b.id)&&(b.commentId=v.id);if(s&&mn(v,c),k(),s&&c.kind==="folder"&&v.kind!=="create"){let b=vn(v.text);for(let E=0;E<b;E++)try{await xt(v,c)}catch{break}}return ie(c.id),u(t,200,v)}let I=/^\/api\/comments\/([^/]+)(?:\/(\w+))?$/.exec(i);if(I){let n=d.comments.find(f=>f.id===I[1]);if(!n)return u(t,404,{error:"no such comment"});let c=I[2];if(e.method==="DELETE"&&!c){if(!s&&!n.author?.guest)return u(t,403,{});let f=new Set([n.id,...d.comments.filter(w=>w.parentId===n.id).map(w=>w.id)]),h=S.get(n.frameId);return h&&f.has(h.commentId)&&(h.canceled=!0,h.child?h.child.kill("SIGTERM"):S.delete(n.frameId)),d.comments=d.comments.filter(w=>!f.has(w.id)),d.strokes=d.strokes.filter(w=>!f.has(w.commentId)),k(),u(t,200,{ok:!0})}if(!s)return u(t,403,{error:"host only"});if(c==="send")return["waiting","error","canceled"].includes(n.status)&&(n.status="queued",n.snapshot=null,n.undone=!1,n.redoSnap=null,k(),ie(n.frameId)),u(t,200,n);if(c==="background")return s?n.status!=="queued"?u(t,409,{error:"It already started"}):(n.background=!0,k(),ie(n.frameId),u(t,200,{ok:!0})):u(t,403,{});if(c==="steer")return s?kn(n)?u(t,200,{ok:!0}):u(t,409,{error:"Nothing to steer into: the run already finished"}):u(t,403,{});if(c==="cancel"){let f=S.get(n.frameId);return f?.commentId===n.id?(f.canceled=!0,f.child?f.child.kill("SIGTERM"):V(n,"canceled","Stopped.")):n.status==="queued"&&(n.status="canceled",k()),u(t,200,{ok:!0})}if(c==="undo"){let f=H(n.frameId);if(!n.snapshot||!f||f.kind!=="folder")return u(t,400,{error:"nothing to undo"});if(S.has(f.id))return u(t,409,{error:"The agent is still working on this frame \u2014 wait for it to finish (or stop it) first."});let h=d.comments.filter(y=>y.id!==n.id&&y.frameId===n.frameId&&y.snapshot&&y.status==="done"&&!y.undone&&(y.startedAt||y.createdAt)>(n.startedAt||n.createdAt)),w=await L(e);if(h.length&&!w.force)return u(t,409,{error:"newer",newer:h.length});n.redoSnap=await Ae(f,`${n.id}-after`),await _e(f,n.snapshot),n.undone=!0;for(let y of h)y.undone=!0;return k(),Z({type:"frame-changed",frameId:f.id}),u(t,200,{ok:!0,reverted:1+h.length})}if(c==="redo"){let f=H(n.frameId);return!n.redoSnap||!n.undone||!f?u(t,400,{error:"nothing to redo"}):S.has(f.id)?u(t,409,{error:"The agent is still working on this frame \u2014 wait for it to finish first."}):(await _e(f,n.redoSnap),n.undone=!1,k(),Z({type:"frame-changed",frameId:f.id}),u(t,200,{ok:!0}))}if(c==="resolve")return n.resolved=!n.resolved,k(),u(t,200,n);if(c==="allow"&&e.method==="POST"){let f=await L(e),h=(f.all?n.blocked||[]:[n.blocked?.[+f.i]]).filter(M=>M&&!M.allowed);if(!h.length)return u(t,400,{error:"Nothing to allow"});let w=d.agent.allow||=[];for(let M of h)w.includes(M.rule)||w.push(M.rule),M.allowed=!0;let y=B.get(n.frameId);y&&!y.turn?pe(y):y&&(y.restart=!0);let v=d.comments.find(M=>M.id===(n.parentId||n.id)),b=h.map(M=>M.tool==="Bash"?`\`${M.label}\``:M.label.charAt(0).toLowerCase()+M.label.slice(1)),E=b.length>1?`${b.slice(0,-1).join(", ")} and ${b[b.length-1]}`:b[0];return d.comments.push({id:N(8),frameId:n.frameId,parentId:v?.id||n.id,anchor:n.anchor,ink:[],inkStrokes:[],text:`Allowed ${E}. Go ahead and use ${b.length>1?"them":"it"}, then carry on with what you were doing.`,author:v?.author||{name:"You",color:"#2F5BFF"},status:"queued",reply:null,createdAt:Date.now()}),k(),ie(n.frameId),u(t,200,{ok:!0})}if(c==="note"){let f=await L(e),h=String(f.text||"").trim().slice(0,2e3);if(!h)return u(t,400,{error:"text required"});let w={id:N(8),kind:"note",frameId:n.frameId,parentId:n.parentId||n.id,text:h,anchor:n.anchor,author:{name:ue[xe()].label,agent:!0,color:"#17171A"},model:f.model?String(f.model).slice(0,60):void 0,status:"done",createdAt:Date.now()};return d.comments.push(w),k(),Z({type:"frame-changed",frameId:n.frameId}),u(t,200,w)}if(c==="step"){let f=await L(e);return J(n.frameId,String(f.text||"").slice(0,120)),u(t,200,{ok:!0})}if(c==="option"&&e.method==="POST"){let f=await L(e),h=H(n.frameId);if(!h)return u(t,404,{error:"no frame"});if(n.options=n.options||[],h.kind==="folder"){let v=String(f.name||"").trim().slice(0,60),b=n.options.find(E=>E.placeholder&&E.frameId);try{if(b){b.placeholder=!1,b.name=v||b.name;let E=d.frames.find(M=>M.id===b.frameId);E&&v&&(E.name=he(`${b.id} \xB7 ${v}`)),b.madeAt=Date.now(),k()}else b=await xt(n,h,v||`Option ${String.fromCharCode(65+n.options.length)}`)}catch(E){return u(t,400,{error:E.message})}return u(t,200,{id:b.id,dir:b.dir,root:h.src,frame:!0})}if(n.options.length>=6)return u(t,400,{error:"up to 6 options"});let w=String.fromCharCode(65+n.options.length),y=m.join(O,"options",n.id,w);return await P.rm(y,{recursive:!0,force:!0}),await P.mkdir(y,{recursive:!0}),n.options.push({id:w,name:String(f.name||`Option ${w}`).slice(0,60),dir:y}),k(),u(t,200,{id:w,dir:y,root:Mt(h)})}if(c==="thumbs"&&e.method==="POST")return Ft(n),u(t,200,{ok:!0});if(c==="preview"&&e.method==="POST"){if(!s)return u(t,403,{});let f=await L(e);try{await yt(n,f.option||null)}catch(h){return u(t,400,{error:h.message})}return u(t,200,{ok:!0})}if(c==="clearoptions"&&e.method==="POST"){let f=new Set((n.options||[]).map(h=>h.frameId).filter(Boolean));for(let h of f){let w=S.get(h);w&&(w.canceled=!0,w.child?.kill("SIGTERM"),S.delete(h)),Pe(h)}d.frames=d.frames.filter(h=>!f.has(h.id)),d.comments=d.comments.filter(h=>!f.has(h.frameId)),d.sections=d.sections.filter(h=>h.id!==n.optionSection);for(let h of n.options||[])delete h.frameId;return n.optionsCleared=!0,delete n.optionSection,k(),u(t,200,{ok:!0})}if(c==="choose"&&e.method==="POST"){if(!s)return u(t,403,{});let f=await L(e),h=n.options?.find(y=>y.id===f.option&&y.frameId),w=h&&d.frames.find(y=>y.id===h.frameId);if(w){let y=H(n.frameId);if(!y||y.kind!=="folder")return u(t,404,{error:"The original frame is gone."});if(S.has(y.id))return u(t,409,{error:"The agent is still working on the original frame. Wait for it to finish (or stop it) first."});let v=`r${N(7)}`;return await Ae(y,v)?((y.restores||=[]).push({id:v,at:Date.now()}),await _e(y,w.src),n.optionChosen=h.id,ce.set(y.id,(ce.get(y.id)||0)+1),k(),Z({type:"frame-changed",frameId:y.id}),u(t,200,{ok:!0})):u(t,500,{error:"Could not keep the current version, so nothing was changed."})}try{await yt(n,f.option)}catch(y){return u(t,400,{error:y.message})}return n.optionChosen=f.option,k(),await P.rm(m.join(O,"options",n.id,"_original"),{recursive:!0,force:!0}).catch(()=>{}),u(t,200,{ok:!0})}if(c==="reply"){let f=await L(e);return S.has(n.frameId)||S.set(n.frameId,{commentId:n.id,step:"",startedAt:Date.now()}),f.model&&(n.model=String(f.model).slice(0,60),d.agent.lastModel=n.model),n.startedAt||(n.startedAt=S.get(n.frameId)?.startedAt),V(n,f.error?"error":"done",String(f.text||"Done.").slice(0,4e3)),u(t,200,n)}}if(!s)return u(t,403,{error:"host only"});if(i==="/api/inbox"){let n=Date.now()+(o.searchParams.get("wait")?25e3:0),c=Re(),f=!1;for(e.on("close",()=>{f=!0}),Ye=Date.now(),c||k(!0);;){if(f)return;Ye=Date.now();let h=d.comments.find(w=>w.status==="queued"&&!w.background&&!S.has(w.frameId));if(h){let w=H(h.frameId),y={commentId:h.id,step:"Your chat picked this up",startedAt:Date.now(),inbox:!0};return S.set(w.id,y),setTimeout(()=>{S.get(w.id)===y&&y.step==="Your chat picked this up"&&J(w.id,"Working\u2026")},3e3),h.status="working",k(),[h.snapshot]=await Promise.all([h.snapshot||rt(w,h.id),gn(h)]),u(t,200,{comment:{id:h.id,kind:h.kind||"comment",text:h.text,anchor:h.anchor,ink:h.ink,annotation:h.annotation,attachments:h.attachments},frame:{id:w.id,name:w.name,kind:w.kind,src:w.src,entry:w.entry,w:w.w,h:w.h},prompt:at(w,h)})}if(Date.now()>n)return u(t,200,{comment:null});await new Promise(w=>{let y=()=>{clearTimeout(v),Be.delete(y),w()},v=setTimeout(y,2e3);Be.add(y)})}}if(e.method==="POST"&&i==="/api/frames"){let n=await L(e);if(n.prompt){let c=n.name||n.prompt.split(/[.\n]/)[0].slice(0,40),f=mt(c),h=Q({src:f,name:c,w:n.w,h:n.h,x:n.x,y:n.y});return d.comments.push({id:N(8),kind:"create",frameId:h.id,text:n.prompt,author:{name:"You",color:"#2F5BFF"},status:"queued",createdAt:Date.now()}),k(),ie(h.id),u(t,200,{...h,dir:f})}if(n.new){let c=mt(n.name||"Untitled",!!n.blank);typeof n.html=="string"&&n.html.length<4e6&&$.writeFileSync(m.join(c,"index.html"),n.html);let f=n.x==null&&In();if(f)return u(t,200,{...Tn(f.c,f.f,c,n.name),dir:c});let h=Q({...n,src:c});return n.parent&&d.frames.some(w=>w.id===n.parent&&w.id!==h.id)&&(h.parent=n.parent,k()),(n.fill==="none"||/^#[0-9a-f]{6}$/i.test(n.fill||""))&&(h.fill=n.fill.toLowerCase(),k()),u(t,200,{...h,dir:c})}return n.route!=null?u(t,200,Q(n)):n.src?!/^https?:\/\//.test(n.src)&&!$.existsSync(m.resolve(le,n.src))?u(t,400,{error:`No such file or folder: ${n.src}`}):u(t,200,Q(n)):u(t,400,{error:"src, route, new or prompt required"})}let _=/^\/api\/frames\/([^/]+)\/versions(\/restore|\/duplicate)?$/.exec(i);if(_&&e.method==="POST"){let n=d.frames.find(b=>b.id===_[1]);if(!n||n.kind!=="folder")return u(t,400,{error:"Only static frames keep versions."});let c=encodeURIComponent(n.entry||"index.html");if(!_[2]){let b=[...d.comments.filter(T=>T.frameId===n.id&&T.snapshot&&!T.snapThumb&&$.existsSync(T.snapshot)),...(n.restores||[]).filter(T=>!T.thumb)].slice(-14),E=async T=>{try{let ee=await de(n,`http://127.0.0.1:${K}/__loop/hist/${T.id}/${c}`);T["frameId"in T?"snapThumb":"thumb"]=`/__loop/shots/${m.basename(ee)}`,k()}catch{}},[M]=await Promise.all([de(n).catch(()=>null),...b.map(E)]);return u(t,200,{now:M?`/__loop/shots/${m.basename(M)}`:null})}let f=await L(e),h=String(f.id||"");if(!/^[A-Za-z0-9_-]+$/.test(h))return u(t,400,{error:"no such version"});let w=d.comments.some(b=>b.id===h&&b.frameId===n.id&&b.snapshot)||(n.restores||[]).some(b=>b.id===h),y=m.join(O,"history",h);if(!w||!$.existsSync(y))return u(t,404,{error:"That version is no longer kept."});if(_[2]==="/duplicate"){let b=he(String(f.name||`${n.name} \xB7 earlier version`).slice(0,80)),E=we(b);return await se(y,E),u(t,200,Q({src:m.join(E,n.entry||"index.html"),name:b,w:n.w,h:n.h,near:n.id}))}if(S.has(n.id))return u(t,409,{error:"The agent is still working on this frame. Wait for it to finish (or stop it) first."});let v=`r${N(7)}`;return await Ae(n,v)?((n.restores||=[]).push({id:v,at:Date.now()}),await _e(n,y),ce.set(n.id,(ce.get(n.id)||0)+1),k(),Z({type:"frame-changed",frameId:n.id}),u(t,200,{ok:!0})):u(t,500,{error:"Could not keep the current version, so nothing was changed."})}let A=/^\/api\/frames\/([^/]+)\/(source|edit)$/.exec(i);if(A){let n=d.frames.find(h=>h.id===A[1]);if(!n||!s)return u(t,404,{error:"no such frame"});if(n.kind!=="folder")return u(t,400,{error:"Only static frames can be edited directly."});let c=m.join(n.src,n.entry||"index.html"),f;try{f=await P.readFile(c,"utf8")}catch{return u(t,404,{error:"The frame has no page to edit."})}if(A[2]==="source"&&e.method==="GET")return u(t,200,{text:f});if(A[2]==="edit"&&e.method==="POST"){if(S.has(n.id))return u(t,409,{error:"The agent is working on this frame. Try again when it is done."});let h=await L(e),w=(Array.isArray(h.edits)?h.edits:[]).map(T=>({a:T.a|0,b:T.b|0,before:String(T.before??""),text:String(T.text??"")})).sort((T,ee)=>T.a-ee.a);if(!w.length||w.length>40)return u(t,400,{error:"nothing to change"});let y=0;for(let T of w){if(T.a<y||T.b<T.a||T.b>f.length||f.slice(T.a,T.b)!==T.before)return u(t,409,{error:"The page changed since you started editing. Try again."});y=T.b}let v=pt.get(n.id)||0,b=Date.now();if(b-v>9e4){let T=`e${N(7)}`;await Ae(n,T)&&(n.restores||=[]).push({id:T,at:b,what:String(h.what||"Edited by hand").slice(0,80)})}pt.set(n.id,b);let E="",M=0;for(let T of w)E+=f.slice(M,T.a)+T.text,M=T.b;return E+=f.slice(M),await P.writeFile(c,E),k(),u(t,200,{ok:!0})}}let F=/^\/api\/frames\/([^/]+)\/warm$/.exec(i);if(F&&e.method==="POST"){let n=H(F[1]);return Ne&&n?.kind!=="canvas"&&Lt().catch(()=>{}),u(t,200,{warm:!!(n&&Rn(n))})}let C=/^\/api\/frames\/([^/]+)\/shot$/.exec(i);if(C&&e.method==="POST"){let n=d.frames.find(h=>h.id===C[1]);if(!n||!s)return u(t,404,{error:"no such frame"});let c=await L(e),f;if(c.option){let[h,w]=String(c.option).split(":"),y=d.comments.find(b=>b.id===h);if(!y?.options?.some(b=>b.id===w))return u(t,400,{error:"no such option"});let v=d.frames.find(b=>b.id===y.options.find(E=>E.id===w)?.frameId);if(v)try{return u(t,200,{path:await de(v)})}catch(b){return u(t,500,{error:b.message})}if(n.kind!=="folder")return u(t,400,{error:"Options on app screens can't be screenshotted before they're applied. Describe them clearly instead."});f=`http://127.0.0.1:${K}/__loop/optview/${h}/${w}/${encodeURIComponent(n.entry||"index.html")}`}S.get(n.id)?.inbox&&J(n.id,"Looking at the screen");try{return u(t,200,{path:await de(n,f)})}catch(h){return u(t,500,{error:h.message})}}let D=/^\/api\/frames\/([^/]+)\/freeze$/.exec(i);if(D&&e.method==="POST"){let n=d.frames.find(v=>v.id===D[1]);if(!n)return u(t,404,{error:"no such frame"});let c=await L(e);if(typeof c.html!="string"||!c.html)return u(t,400,{error:"nothing captured"});let f=he(`${n.name.replace(/ · frozen( \d+)?$/,"")} \xB7 frozen`),h=we(f);await P.mkdir(h,{recursive:!0}),await P.writeFile(m.join(h,"index.html"),c.html);let w=typeof c.from=="string"&&c.from.startsWith("/")&&!c.from.startsWith("/__loop/")?c.from.slice(0,2e3):n.kind==="frozen"?n.from:n.kind==="route"?n.src:void 0,y={id:N(6),name:f,kind:"frozen",src:h,entry:"index.html",from:w,frozenAt:Date.now(),w:n.w,h:n.h,keyboard:n.keyboard,...c.x!=null&&c.y!=null?{x:+c.x,y:+c.y}:Je(n.w,n.h,n.id),createdAt:Date.now()};return d.frames.push(y),k(),u(t,200,y)}let R=/^\/api\/frames\/([^/]+)\/duplicate$/.exec(i);if(R&&e.method==="POST"){let n=d.frames.find(b=>b.id===R[1]);if(!n)return u(t,404,{error:"no such frame"});let c=await L(e),f=n.kind==="route"&&typeof c.route=="string"&&c.route.startsWith("/")&&!c.route.startsWith("/__loop/")?c.route.slice(0,2e3):null,h=f&&f!==n.src,w=he(h?ln(f):`${n.name.replace(/ copy( \d+)?$/,"")} copy`),y=c.x!=null&&c.y!=null?{x:c.x,y:c.y}:{near:n.id},v;if(n.kind==="folder"||n.kind==="frozen"){let b=we(w);await se(n.src,b),v=Q({src:m.join(b,n.entry),name:w,w:n.w,h:n.h,...y}),n.kind==="frozen"&&(Pe(v.id),Object.assign(v,{kind:"frozen",from:n.from,frozenAt:n.frozenAt}),k())}else n.kind==="route"?v=Q({route:f||n.src,name:w,w:n.w,h:n.h,dup:!0,...y}):v=Q({src:n.src,name:w,w:n.w,h:n.h,...y});return u(t,200,v)}let W=/^\/api\/frames\/([^/]+)$/.exec(i);if(W){let n=d.frames.find(c=>c.id===W[1]);if(!n)return u(t,404,{});if(e.method==="PATCH"){let c=await L(e);for(let f of["x","y","w","h"])c[f]!=null&&isFinite(c[f])&&(n[f]=Math.round(+c[f]));return c.name&&(n.name=String(c.name).slice(0,80)),typeof c.keyboard=="boolean"&&(n.keyboard=c.keyboard||void 0),"fill"in c&&(n.fill=c.fill==="none"?"none":typeof c.fill=="string"&&/^#[0-9a-f]{6}$/i.test(c.fill)?c.fill.toLowerCase():void 0),"parent"in c&&(n.parent=c.parent&&c.parent!==n.id&&d.frames.some(f=>f.id===c.parent)?c.parent:void 0),k(),u(t,200,n)}if(e.method==="DELETE"){let c=S.get(n.id);return c&&(c.canceled=!0,c.child?.kill("SIGTERM"),S.delete(n.id)),d.frames=d.frames.filter(f=>f.id!==n.id),d.comments=d.comments.filter(f=>f.frameId!==n.id),Pe(n.id),k(),u(t,200,{ok:!0})}}if(e.method==="POST"&&i==="/api/sections"){let n=await L(e);if(![n.x,n.y,n.w,n.h].every(Number.isFinite))return u(t,400,{error:"x, y, w and h required"});let c=d.sections.length+1;for(;d.sections.some(h=>h.name===`Section ${c}`);)c++;let f={id:N(6),name:String(n.name||`Section ${c}`).slice(0,80),x:Math.round(n.x),y:Math.round(n.y),w:Math.max(120,Math.round(n.w)),h:Math.max(120,Math.round(n.h)),createdAt:Date.now()};return d.sections.push(f),k(),u(t,200,f)}let Y=/^\/api\/sections\/([^/]+)$/.exec(i);if(Y){let n=d.sections.find(c=>c.id===Y[1]);if(!n)return u(t,404,{});if(e.method==="PATCH"){let c=await L(e);for(let f of["x","y","w","h"])c[f]!=null&&isFinite(c[f])&&(n[f]=Math.round(+c[f]));return c.name&&(n.name=String(c.name).slice(0,80)),k(),u(t,200,n)}if(e.method==="DELETE")return d.sections=d.sections.filter(c=>c.id!==n.id),k(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/restore"){let n=await L(e);for(let c of n.sections||[])c?.id&&!d.sections.find(f=>f.id===c.id)&&d.sections.push(c);for(let c of n.frames||[])c?.id&&!d.frames.find(f=>f.id===c.id)&&(d.frames.push(c),ot(c));for(let c of n.strokes||[])c?.id&&!d.strokes.find(f=>f.id===c.id)&&d.strokes.push(c);for(let c of n.comments||[])c?.id&&!d.comments.find(f=>f.id===c.id)&&d.comments.push({...c,status:["working","queued"].includes(c.status)?"canceled":c.status});return k(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/strokes"){let n=await L(e),c={id:N(8),points:(n.points||[]).slice(0,6e3).map(h=>Math.round(h*10)/10),color:String(n.color||"#E5484D").slice(0,16),width:+n.width||3,frameId:n.frameId||null,createdAt:Date.now()},f=c.frameId&&d.frames.find(h=>h.id===c.frameId);return f&&(c.fx=Number.isFinite(+n.fx)?+n.fx:f.x,c.fy=Number.isFinite(+n.fy)?+n.fy:f.y),d.strokes.push(c),k(),u(t,200,c)}let Se=/^\/api\/strokes\/([^/]+)$/.exec(i);if(Se&&e.method==="DELETE")return d.strokes=d.strokes.filter(n=>n.id!==Se[1]),k(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/update")return!q||q.how!=="restart"?u(t,400,{error:"No update to apply here"}):q.state==="updating"?u(t,200,{ok:!0}):S.size?u(t,409,{error:"The agent is working. Update when it has finished."}):(Hn(),u(t,200,{ok:!0}));if(e.method==="POST"&&i==="/api/settings"){let n=await L(e);if((n.mode==="headless"||n.mode==="inbox")&&(d.agent.mode=n.mode),n.runner&&ue[n.runner]){if(!ue[n.runner].bin)return u(t,400,{error:`${ue[n.runner].label} isn't installed on this computer`});d.agent.runner=n.runner}if(n.proxy!==void 0){if(n.proxy&&!/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(n.proxy))return u(t,400,{error:"proxy must be a local dev server origin, e.g. http://localhost:3000"});d.proxy=n.proxy||null}if(n.chat&&typeof n.chat=="object"){let c=f=>typeof f=="string"&&/^[\w.:\-\[\]]{1,60}$/.test(f)?f:null;d.agent.chat={runner:n.chat.runner==="codex"?"codex":"claude",model:c(n.chat.model),effort:c(n.chat.effort)}}if(n.pick&&typeof n.pick=="object"){let c=xe(),f=ve(c),h=(d.agent.pick||={})[c]||={};if("model"in n.pick){if(n.pick.model&&!f.models.includes(n.pick.model))return u(t,400,{error:"No such model"});h.model=n.pick.model||void 0}if("effort"in n.pick){if(n.pick.effort&&!ve(c).efforts.includes(n.pick.effort))return u(t,400,{error:"No such effort level"});h.effort=n.pick.effort||void 0}}if(n.disallow){d.agent.allow=(d.agent.allow||[]).filter(c=>c!==n.disallow);for(let c of[...B.values()])c.turn?c.restart=!0:pe(c)}return n.name&&(d.name=String(n.name).slice(0,80)),n.hostName!==void 0&&(d.hostName=String(n.hostName).trim().slice(0,32)||void 0),k(),ct(),u(t,200,{ok:!0})}if(e.method==="POST"&&i==="/api/share/public")try{return await Wn(),u(t,200,Ve())}catch(n){return u(t,400,{error:n.message})}if(e.method==="POST"&&i==="/api/share/stop")return fe?.kill(),ne=null,k(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/share/rotate")return d.token=N(16),k(),u(t,200,Ve());if(i.startsWith("/api/cloud")&&!z)return u(t,404,{error:"Email sharing is not available in this version of Loop."});if(i==="/api/cloud"&&e.method==="GET")return u(t,200,await z.status());if(e.method==="POST"&&i==="/api/cloud/session"){let n=await L(e);return u(t,200,{user:await z.setSession(n)})}if(e.method==="POST"&&i==="/api/cloud/signout")return z.signOut(),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/cloud/publish")return u(t,200,{link:await z.publish()});if(e.method==="POST"&&i==="/api/cloud/snapshot"){let n=await L(e),c=d.frames.find(h=>h.id===n.frameId);if(!c||c.kind!=="route"||typeof n.html!="string")return u(t,400,{error:"route frame and html required"});let f=await z.publishSnapshot(c,n.html);return z.schedulePush(200),u(t,200,f)}if(e.method==="POST"&&i==="/api/cloud/invite"){let n=await L(e);return u(t,200,await z.invite(n.emails||[],n.role))}if(e.method==="DELETE"&&i.startsWith("/api/cloud/members/"))return await z.removeMember(decodeURIComponent(i.slice(19))),u(t,200,{ok:!0});if(e.method==="POST"&&i==="/api/shutdown")return u(t,200,{ok:!0}),setTimeout(We,50);u(t,404,{error:"not found"})}catch(a){u(t,500,{error:a.message})}});Ce.on("upgrade",(e,t,o)=>{let i=new URL(e.url,"http://x");if(!d.proxy||i.pathname.startsWith(re)||!Gt(e,i))return t.destroy();let r=new URL(d.proxy),s=It.connect(+(r.port||80),r.hostname,()=>{let a=`${e.method} ${e.url} HTTP/1.1\r
29
+ `;for(let l=0;l<e.rawHeaders.length;l+=2){let p=e.rawHeaders[l],g=e.rawHeaders[l+1];p.toLowerCase()==="host"&&(g=r.host),a+=`${p}: ${g}\r
30
30
  `}s.write(a+`\r
31
- `),o?.length&&s.write(o),s.pipe(t),t.pipe(s)});s.on("error",()=>t.destroy()),t.on("error",()=>s.destroy())});Jt&&(G={createCloud:()=>null}.createCloud({state:d,changed:k,pump:ie,broadcast:Z,log:e=>console.log(e)}));var bt=!1;setInterval(()=>{let e=Le();e!==bt&&(bt=e,k(!0));for(let t of[...U.values()])!t.turn&&t.frameId!==ke&&!d.frames.some(o=>o.id===t.frameId)&&pe(t);if(d.agent.mode==="inbox"&&!e){for(let[t,o]of x)if(o.inbox){x.delete(t);let i=d.comments.find(r=>r.id===o.commentId);i&&i.status==="working"&&(i.status="queued",k())}if(d.agent.mode==="inbox"&&St>=0){let t=!1;for(let o of d.comments)o.status==="queued"&&!o.background&&Date.now()-(o.queuedAt||o.createdAt)>St&&z(o.frameId)&&(o.background=!0,t=!0);t&&(k(),at())}}},5e3);var St=Number(process.env.LOOP_CHAT_WAIT??15e3),Ve=(()=>{try{return JSON.parse($.readFileSync(m.join(ye,"package.json"),"utf8")).version||null}catch{return null}})(),Qe=process.env.LOOP_UPDATE_TEST?"test":/[\\/]_npx[\\/]/.test(ye)?"npx":/[\\/]node_modules[\\/]/.test(ye)?"global":null,V=null,Rn=(e,t)=>{let o=String(e).split(".").map(Number),i=String(t).split(".").map(Number);for(let r=0;r<3;r++)if((o[r]||0)!==(i[r]||0))return(o[r]||0)>(i[r]||0);return!1};async function Nt(){if(!(!Qe||!Ve||V?.state==="updating"))try{let e=process.env.LOOP_UPDATE_TEST||(await(await fetch("https://registry.npmjs.org/loop-canvas/latest",{signal:AbortSignal.timeout(8e3),headers:{accept:"application/json"}})).json()).version;e&&Rn(e,Ve)&&V?.latest!==e&&(V={latest:e,how:Qe==="npx"?"next-start":"restart",state:"available"},k())}catch{}}setTimeout(Nt,4e3).unref?.();setInterval(Nt,3*36e5).unref?.();async function Nn(){if(V.state="updating",delete V.error,k(),Qe==="global")try{await new Promise((e,t)=>Ht("npm",["install","-g",`loop-canvas@${V.latest}`],{timeout:18e4},(o,i,r)=>o?t(new Error(String(r||o.message).trim().split(`
32
- `).slice(-3).join(" "))):e()))}catch(e){V.state="failed",V.error=e.message.slice(0,300),k();return}tt(),Te(process.execPath,[m.join(ye,"server.mjs"),"--dir",O,"--port",String(X)],{detached:!0,stdio:"ignore",env:{...process.env,LOOP_SAME_PORT:"1",LOOP_UPDATE_TEST:""}}).unref(),setTimeout(Re,150)}function qe(e,t=20){Ee.once("error",o=>{if(o.code==="EADDRINUSE"&&process.env.LOOP_SAME_PORT&&t>0)return setTimeout(()=>qe(e,t-1),500);o.code==="EADDRINUSE"&&t>0?qe(e+1,t-1):(console.error(o),process.exit(1))}),Ee.listen(e,"0.0.0.0",()=>{X=Ee.address().port,$.writeFileSync(vt,JSON.stringify({port:X,pid:process.pid})),console.log(`Loop \u25B8 http://localhost:${X} (${O})`),at()})}function Re(){try{$.rmSync(vt,{force:!0})}catch{}le?.kill(),Ye();for(let e of U.values())try{e.child.kill("SIGTERM")}catch{}for(let e of x.values())e.child?.kill("SIGTERM");clearTimeout(Ge),tt(),process.exit(0)}process.on("SIGINT",Re);process.on("SIGTERM",Re);qe(+$t.port||4848);function Wn(e){let t={};for(let o=0;o<e.length;o++)if(e[o].startsWith("--")){let i=e[o].slice(2);t[i]=e[o+1]&&!e[o+1].startsWith("--")?e[++o]:!0}return t}function me(e){let t=[...(process.env.PATH||"").split(":"),m.join(se.homedir(),".local/bin"),"/opt/homebrew/bin","/usr/local/bin"];for(let o of t){if(!o)continue;let i=m.join(o,e);try{return $.accessSync(i,$.constants.X_OK),i}catch{}}return null}export{we as PRESETS};
31
+ `),o?.length&&s.write(o),s.pipe(t),t.pipe(s)});s.on("error",()=>t.destroy()),t.on("error",()=>s.destroy())});Yt&&(z={createCloud:()=>null}.createCloud({state:d,changed:k,pump:ie,broadcast:Z,log:e=>console.log(e)}));var St=!1;setInterval(()=>{let e=Re();e!==St&&(St=e,k(!0));for(let t of[...B.values()])!t.turn&&t.frameId!==be&&!d.frames.some(o=>o.id===t.frameId)&&pe(t);if(d.agent.mode==="inbox"&&!e){for(let[t,o]of S)if(o.inbox){S.delete(t);let i=d.comments.find(r=>r.id===o.commentId);i&&i.status==="working"&&(i.status="queued",k())}if(d.agent.mode==="inbox"&&$t>=0){let t=!1;for(let o of d.comments)o.status==="queued"&&!o.background&&Date.now()-(o.queuedAt||o.createdAt)>$t&&H(o.frameId)&&(o.background=!0,t=!0);t&&(k(),ct())}}},5e3);var $t=Number(process.env.LOOP_CHAT_WAIT??15e3),Qe=(()=>{try{return JSON.parse($.readFileSync(m.join(ke,"package.json"),"utf8")).version||null}catch{return null}})(),qe=process.env.LOOP_UPDATE_TEST?"test":/[\\/]_npx[\\/]/.test(ke)?"npx":/[\\/]node_modules[\\/]/.test(ke)?"global":null,q=null,zn=(e,t)=>{let o=String(e).split(".").map(Number),i=String(t).split(".").map(Number);for(let r=0;r<3;r++)if((o[r]||0)!==(i[r]||0))return(o[r]||0)>(i[r]||0);return!1};async function zt(){if(!(!qe||!Qe||q?.state==="updating"))try{let e=process.env.LOOP_UPDATE_TEST||(await(await fetch("https://registry.npmjs.org/loop-canvas/latest",{signal:AbortSignal.timeout(8e3),headers:{accept:"application/json"}})).json()).version;e&&zn(e,Qe)&&q?.latest!==e&&(q={latest:e,how:qe==="npx"?"next-start":"restart",state:"available"},k())}catch{}}setTimeout(zt,4e3).unref?.();setInterval(zt,3*36e5).unref?.();async function Hn(){if(q.state="updating",delete q.error,k(),qe==="global")try{await new Promise((e,t)=>Jt("npm",["install","-g",`loop-canvas@${q.latest}`],{timeout:18e4},(o,i,r)=>o?t(new Error(String(r||o.message).trim().split(`
32
+ `).slice(-3).join(" "))):e()))}catch(e){q.state="failed",q.error=e.message.slice(0,300),k();return}nt(),Oe(process.execPath,[m.join(ke,"server.mjs"),"--dir",O,"--port",String(K)],{detached:!0,stdio:"ignore",env:{...process.env,LOOP_SAME_PORT:"1",LOOP_UPDATE_TEST:""}}).unref(),setTimeout(We,150)}function et(e,t=20){Ce.once("error",o=>{if(o.code==="EADDRINUSE"&&process.env.LOOP_SAME_PORT&&t>0)return setTimeout(()=>et(e,t-1),500);o.code==="EADDRINUSE"&&t>0?et(e+1,t-1):(console.error(o),process.exit(1))}),Ce.listen(e,"0.0.0.0",()=>{K=Ce.address().port,$.writeFileSync(Ot,JSON.stringify({port:K,pid:process.pid})),console.log(`Loop \u25B8 http://localhost:${K} (${O})`),ct()})}function We(){try{$.rmSync(Ot,{force:!0})}catch{}fe?.kill(),Xe();for(let e of B.values())try{e.child.kill("SIGTERM")}catch{}for(let e of S.values())e.child?.kill("SIGTERM");clearTimeout(He),nt(),process.exit(0)}process.on("SIGINT",We);process.on("SIGTERM",We);et(+Tt.port||4848);function Un(e){let t={};for(let o=0;o<e.length;o++)if(e[o].startsWith("--")){let i=e[o].slice(2);t[i]=e[o+1]&&!e[o+1].startsWith("--")?e[++o]:!0}return t}function me(e){let t=[...(process.env.PATH||"").split(":"),m.join(ae.homedir(),".local/bin"),"/opt/homebrew/bin","/usr/local/bin"];for(let o of t){if(!o)continue;let i=m.join(o,e);try{return $.accessSync(i,$.constants.X_OK),i}catch{}}return null}export{ye as PRESETS};
@@ -29,11 +29,12 @@ If a global install isn't allowed, run every command below as `npx -y loop-canva
29
29
  - "Put this project (or its main screens) on a canvas": run `loop scan` from the project root. It detects the framework (Next.js, SvelteKit, Nuxt, Astro, Remix, React Router/Vite SPAs, Angular, or plain HTML), starts the project's dev server if it isn't running, and places **one live frame of the app** (its home screen, served through Loop so the user can point at exact elements). The user clicks around inside it, and duplicates it (⌘D or ⌥-drag the title) to keep another screen side by side; the copy opens on the screen they navigated to. Options: `--mobile` (phone, 393×852), `--landscape` (phone landscape, 852×393, good for games), `--w W --h H` (any size), `--both` (desktop + phone), `--url http://localhost:PORT` (use a specific running server).
30
30
  Add a specific screen only when the user asks for it: `loop add-route /settings --name "Settings"`. Don't place every screen: each live frame runs the whole app. `loop scan --all` exists, but only use it when the user explicitly asks for every screen at once.
31
31
  If the app can't run (missing env vars, backend down), say so and ask which few screens matter; then build those as small static frames (step 2), never a copy of the whole app per frame.
32
+ A live frame only shows something while its app's server is running. If you start that server yourself, start it so it keeps running after your turn ends (in the background, detached), or the frames go blank the moment you finish. Never give Loop its own address (`localhost:<Loop's port>`) as a frame: that is the canvas, not the app.
32
33
  - "Explore / give me N directions for X": make N frames, one per distinct direction. Make them genuinely different (layout, hierarchy, tone), not one design with small tweaks.
33
34
  2. **Create a static frame:** `loop new "Pricing — editorial"` prints a folder path. Write a complete, self-contained `index.html` there: inline CSS/JS, CDN imports are fine, no build step, realistic content, working interactions. The canvas reloads the frame live as you write.
34
35
  - Sizes: `--w 1280 --h 800` desktop (default) · `--w 390 --h 844` mobile · `--w 834 --h 1112` tablet.
35
36
  - An existing folder or HTML file can be placed directly: `loop add ./prototypes/onboarding --name "Onboarding"`.
36
- 3. **Open it:** `loop open` (`loop scan` already does this). It starts Loop's server for this project in the background (state lives in `./.loop/`), prints a URL like `http://localhost:4848/__loop/`, and returns. Open **that exact URL**:
37
+ 3. **Open it last:** finish the canvas before the user sees it. Every command above starts Loop's server by itself, so add all the frames first, look at each with `loop shot <frame>` (a blank or wrong frame is fixed now, not in front of the user), and only then run `loop open`. Run by an agent, `loop scan` does not open the browser and `loop open` leaves an empty canvas closed. `loop open` prints a URL like `http://localhost:4848/__loop/` and returns (state lives in `./.loop/`). Open **that exact URL**:
37
38
  - In Codex, open it in Codex's built-in browser view. If you can't open a browser, give the user the URL to open.
38
39
  - In Claude Code, it opens in the user's browser automatically.
39
40
  The canvas is only ever that localhost page. If you find yourself writing HTML for a canvas, toolbar or comment UI, or publishing a site, stop: that's not Loop, and comments left there reach no one.