thinkpool-pair 0.7.447 → 0.7.448

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -227,6 +227,31 @@ their own sandbox cannot bind a port or launch a browser:
227
227
  Preview roots are read-only, cannot escape the lane worktree, and accept only
228
228
  the preview server’s loopback origin for page requests.
229
229
 
230
+ The browser starts on demand and is reused while work is active. After the
231
+ last active or queued job finishes, 60 seconds of inactivity closes it; the
232
+ next job starts it again. Each job uses a fresh browser context, disposed on
233
+ success or failure. Desktop (1440×900) and mobile (390×844) run sequentially.
234
+ Mobile enables touch, mobile viewport behavior, and a mobile Chromium identity
235
+ at device scale 1. It does not claim to emulate Safari or a physical iPhone.
236
+
237
+ Managed preview jobs across all rooms share one host slot with the repository
238
+ browser checks and the Design render/freeze helpers, including the browser
239
+ tests in `test:unit`. The unit runner also uses one worker. The slot uses an exclusive loopback listener on port 47683;
240
+ it serves no application protocol and disappears if its owning process exits.
241
+ Waiting jobs time out after two minutes without stealing an active slot.
242
+ Personal Chrome tabs and independently launched browser scripts are outside
243
+ this managed limit.
244
+
245
+ From the repository, run `npm run test:browser` for sequential Chromium and
246
+ WebKit viewport, touch/click, and session-isolation checks. Use
247
+ `npm run test:browser -- --engine=chromium` on older Macs without supported
248
+ WebKit. `TP_BROWSER_PATH` also selects an installed Chrome for the app tests
249
+ and Design helpers when bundled Chromium cannot run on the host. CI installs
250
+ both pinned Playwright engines and runs the existing landing focus and Lynx
251
+ request-lifecycle app tests under WebKit (`TP_TEST_BROWSER_ENGINE=webkit`).
252
+ Safari-sensitive releases still require a real-device check of login, keyboard/focus, scrolling, and the
253
+ changed interaction; record the device/OS and result separately.
254
+
230
255
  ## Optional bridge environment variables
231
256
 
232
257
  | Variable | Purpose |
@@ -0,0 +1 @@
1
+ import o from"node:net";import{setTimeout as t}from"node:timers/promises";export const BROWSER_JOB_PORT=47683;export async function acquireBrowserJob({port:r=47683,timeoutMs:e=12e4,signal:s}={}){if(!Number.isInteger(r)||r<1||r>65535)throw new Error("Invalid browser job port.");const i=Date.now()+e;for(;;){s?.throwIfAborted();const e=o.createServer(o=>o.destroy());try{await new Promise((o,t)=>{e.once("error",t),e.listen({host:"127.0.0.1",port:r,exclusive:!0},o)})}catch(o){if(e.close(),"EADDRINUSE"!==o.code)throw o;if(Date.now()>=i)throw new Error("Browser job queue timed out. Another job still holds this host’s browser slot.");await t(Math.min(50,Math.max(1,i-Date.now())),void 0,{signal:s});continue}let n=!1;const a=async()=>{n||(n=!0,await new Promise((o,t)=>e.close(r=>r?t(r):o())))};return s?.aborted&&(await a(),s.throwIfAborted()),a}}export async function withBrowserJob(o,t){const r=await acquireBrowserJob(t);try{return await o()}finally{await r()}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.447",
3
+ "version": "0.7.448",
4
4
  "description": "Connect Claude Code, Codex, or Hermes on your computer to a thinkpool Code room.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -127,6 +127,7 @@
127
127
  "flow-preview.mjs",
128
128
  "mockup-delivery.mjs",
129
129
  "viewport.mjs",
130
+ "browser-job.mjs",
130
131
  "design-edit.mjs",
131
132
  "realtime-auth.mjs",
132
133
  "design-source-contract.mjs",
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "format": 1,
3
3
  "package": "thinkpool-pair",
4
- "version": "0.7.447",
4
+ "version": "0.7.448",
5
5
  "kind": "minified",
6
- "minifiedModules": 149,
7
- "sourceModuleBytes": 1971030,
8
- "artifactModuleBytes": 900280,
6
+ "minifiedModules": 150,
7
+ "sourceModuleBytes": 1976056,
8
+ "artifactModuleBytes": 903265,
9
9
  "files": {
10
- "package.json": "cbd3399526cf76070bda7c22b203565fedff31dfb061f25e41a8271905058865",
10
+ "package.json": "a1aa09ae41663a65320fdea0ec238fb6e543364ae97a2819ff1b25fa160160b0",
11
11
  "LICENSE": "8a030197ca1cad9669909ca14c9ad15cf48d9fe551da526ccaf1fca993eddb23",
12
12
  "bridge.mjs": "1bab3b6045603450a92eb3199568e05a8842a18dcfb31fdbe93d413c7fb1ae7f",
13
13
  "bridge-service.sh": "b64f27c473ca8f3a49190fd08c23947ab14ca9958d70b6fa53c7bddc2be91403",
@@ -35,7 +35,7 @@
35
35
  "question-response.mjs": "35a7ba53d3eb117717c18004d2a8441f8f37665b231a3c44f4bd9321ff59dd57",
36
36
  "thinkpool-room-prompt.mjs": "cbae9bba173afad90eec21cfc726baec72cc26de4e5b96c25098755fc61f02c8",
37
37
  "thinkpool-prompt-contracts.mjs": "051602d7d2fe9b34df92957bd3922c5eeebedbcc60c892adf415243887d8c62f",
38
- "thinkpool-capabilities.json": "d4143c15c370f42cb48a432182ef6aa20e9a45a710b62f7469675e7ce25fd937",
38
+ "thinkpool-capabilities.json": "9a47bb1e0cf69f8ca0af9dc7685aa9ac6745393900c46e22274d3eedb8626905",
39
39
  "room-file-delivery.mjs": "a33300b7f12152e8d016771aabbd4616896dec4bf7a3762e059d271e107a1b6a",
40
40
  "codex-app-server.mjs": "f49c4f6c15766765074b014eee11e9399f6c540420a1157f6e9ad79a432b18e3",
41
41
  "codex-images.mjs": "31605d6d8b3dd6b73395da63d1b0c513445c95ffa2f4306ee466f201d633ffc0",
@@ -126,7 +126,8 @@
126
126
  "flow-host-revert.mjs": "a6da239fc54723c74ed51fc03ae3203458b92d2a2e33c26576a9e62cd1031fea",
127
127
  "flow-preview.mjs": "d38cb7f292d41600cde85eef335b0793f5bad7583f9f851f886ef48c4a91b4f1",
128
128
  "mockup-delivery.mjs": "fa112408a186d856b343157381188dc18502aba6cf7ef365459f6c5a074e9cd6",
129
- "viewport.mjs": "80cb9b6e9275499397239a8b4288c0b49791886696a46917ac798acb75267ee5",
129
+ "viewport.mjs": "cdbf8f6617e8b840c1317b91580eff8d2c7f86d3d476651bcb1bdd657f23c101",
130
+ "browser-job.mjs": "391f92c293ba6da419089eb7eaaf942ba44339d1bdba87bb56e23e51d1e94fd0",
130
131
  "design-edit.mjs": "23df0a89b8f6f4df19b7eac55e3bee0ef277c9cddd632d9c3bd0197b2a35059e",
131
132
  "realtime-auth.mjs": "f18260716dbca71f420cafac7f09ce435df56de217174ee50196e9be9cf93ecc",
132
133
  "design-source-contract.mjs": "18bcd024b317293e445c433fc9c59f9eb85e8fb3fa4de824b82604bbae4687b8",
@@ -161,7 +162,7 @@
161
162
  "providers.mjs": "c0757433d30bf2f22e80f85e779cdeeeb10b412d568965f1115d4de2c8aa0314",
162
163
  "provider-resilience.mjs": "a5a3921040d1860bb82f1d3b53c6ec25a5e230345c0131f2213c535e07fc497e",
163
164
  "model-prices.mjs": "91f0bd8688f59ce6bff7743710d3b1139f4f0823d6b95fa68cd9b6fdcdba6ab4",
164
- "README.md": "3e243a1b9c3ad9e7a2b824ccac56e447e6de7f2616b8d3d1c19cc63438f9915b",
165
+ "README.md": "9f423baed047976763c8fbcab81366ba0ce5872844101d8b123d6cdfb375895b",
165
166
  "publish-guard.mjs": "569dd788790e68ea822129f588fd16c1bb435324666552fe9dfe7c435ab3f617"
166
167
  }
167
168
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 44,
3
+ "bundleVersion": 45,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
@@ -182,24 +182,51 @@
182
182
  },
183
183
  {
184
184
  "id": "visual-proof",
185
- "version": 7,
185
+ "version": 8,
186
186
  "routes": [
187
187
  {
188
188
  "id": "visual-proof",
189
- "tools": ["preview_start", "preview_capture", "preview_inspect", "preview_stop"],
189
+ "tools": [
190
+ "preview_start",
191
+ "preview_capture",
192
+ "preview_inspect",
193
+ "preview_stop"
194
+ ],
190
195
  "trigger": "\\b(ui|ux|visual|design|frontend|html|css|page|route|mockup|screenshot|responsive|desktop|mobile|preview)\\b",
191
- "prompt": "For built visual work, run the build, use preview_start, preview_capture at desktop and mobile, preview_inspect when rendered state matters, and preview_stop when finished. Omit the preview path to render the current authenticated room; pass / explicitly only when the public landing page is intended. For an intentional application-preview Design card, use the project's source-aware Design build command when one exists (for example npm run build:design); otherwise build normally and keep selector-based source matching labeled as fallback. preview_capture uses a fresh isolated browser and is inline verification evidence by default; it must not create a transcript card. Set card=true only when the person asked for a mockup/Design artifact or the visual itself is an intentional deliverable that should remain editable in the transcript. A card requires settled meaningful content at both desktop and mobile, rejects loading/public-auth fallback shells, and is delivered after the final agent response. Room and protected routes automatically use the bridge account as a read-only authenticated visual harness; external REST writes, presence, ordinary room broadcasts, and refresh-token access are blocked, while non-mutating roster/transcript snapshot requests are allowed. Surface PNG evidence only when no interactive source-backed Design card is displayed."
196
+ "prompt": "For visual work, build, use preview_start, preview_capture at desktop and mobile, preview_inspect for rendered state, and preview_stop when finished. Omit path for the authenticated room; / targets the public landing page. For application-preview Design cards, use the source-aware Design build command when available; otherwise build normally with selector-based source matching labeled as fallback. Managed previews share one host job slot, reuse a browser, and close it after 60 seconds idle. Each job has an isolated context. Mobile enables touch and mobile layout; Safari/device checks are separate. Captures are inline verification evidence by default and must not create a transcript card. Set card=true only for a requested mockup/Design artifact or intentional editable visual deliverable. Cards require settled desktop+mobile content, reject loading/public-auth fallback shells, and arrive after the final response. Room/protected previews use the bridge account's read-only authenticated harness: external REST writes, presence, ordinary room broadcasts, and refresh-token access are blocked; non-mutating roster/transcript snapshots are allowed. Show PNGs only when no interactive source-backed Design card is displayed."
192
197
  }
193
198
  ],
194
199
  "impact": [
195
- {"path": "bridge/viewport.mjs"},
196
- {"path": ".claude/skills/mockup-iterate/scripts/render.sh"}
200
+ {
201
+ "path": "bridge/viewport.mjs"
202
+ },
203
+ {
204
+ "path": ".claude/skills/mockup-iterate/scripts/render.sh"
205
+ },
206
+ {
207
+ "path": "bridge/browser-job.mjs"
208
+ },
209
+ {
210
+ "path": "scripts/check-browser-viewports.mjs"
211
+ }
197
212
  ],
198
213
  "evidence": [
199
- {"path": "bridge/viewport.mjs", "pattern": "preview_start"},
200
- {"path": "bridge/viewport.mjs", "pattern": "preview_capture"},
201
- {"path": "bridge/viewport.mjs", "pattern": "preview_inspect"},
202
- {"path": "bridge/viewport.mjs", "pattern": "preview_stop"}
214
+ {
215
+ "path": "bridge/viewport.mjs",
216
+ "pattern": "preview_start"
217
+ },
218
+ {
219
+ "path": "bridge/viewport.mjs",
220
+ "pattern": "preview_capture"
221
+ },
222
+ {
223
+ "path": "bridge/viewport.mjs",
224
+ "pattern": "preview_inspect"
225
+ },
226
+ {
227
+ "path": "bridge/viewport.mjs",
228
+ "pattern": "preview_stop"
229
+ }
203
230
  ]
204
231
  },
205
232
  {
package/viewport.mjs CHANGED
@@ -1 +1 @@
1
- import{spawn as e}from"node:child_process";import t from"node:fs";import r from"node:fs/promises";import o from"node:os";import n from"node:path";import{randomUUID as i}from"node:crypto";import{previews as s,startPreview as a}from"./flow-preview.mjs";import{DESIGN_SOURCE_MAP_RELATIVE as l,MAX_DESIGN_SOURCE_MAP_BYTES as c}from"./design-source-contract.mjs";export const DEFAULT_VIEWPORTS=Object.freeze({desktop:Object.freeze({width:1440,height:900}),mobile:Object.freeze({width:390,height:844})});const u=5e3,h=(e,t)=>{const r=n.relative(e,t);return""===r||!r.startsWith("..")&&!n.isAbsolute(r)};export async function resolveContainedRoot(e,o="dist"){if(!e)throw new Error("A lane workspace is required.");if(n.isAbsolute(o))throw new Error("Preview root must be relative to the lane workspace.");const i=await r.realpath(n.resolve(e)),s=n.resolve(i,o||"dist");let a;try{a=await r.realpath(s)}catch{throw new Error(`Preview root does not exist: ${o||"dist"}. Build the app first, or choose a directory containing index.html.`)}if(!h(i,a))throw new Error("Preview root escapes the lane workspace.");if(!(await r.stat(a)).isDirectory())throw new Error("Preview root must be a directory.");if(!t.existsSync(n.join(a,"index.html")))throw new Error(`No index.html found in preview root: ${o||"dist"}.`);return a}export async function resolvePreviewSourceMap(e){if(!e)return null;let t,o;try{t=await r.realpath(e),o=await r.realpath(n.join(t,l));const i=await r.stat(o);if(!i.isFile()||i.size<=0||i.size>c)return null}catch{return null}return h(t,o)?o:null}export function normalizeRoute(e="/"){const t=String(e||"/").trim();if(!t.startsWith("/")||t.startsWith("//"))throw new Error('Preview path must start with one "/" and cannot be a URL.');const r=new URL(t,"http://127.0.0.1");if("http://127.0.0.1"!==r.origin)throw new Error("Preview path cannot change the preview origin.");return`${r.pathname}${r.search}${r.hash}`}export function safeSlug(e="viewport"){return String(e||"viewport").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,56)||"viewport"}export function isSignedOutRoomPreview(e,t){return!!new URL(normalizeRoute(e),"http://127.0.0.1").searchParams.get("r")&&/\bid\s*=\s*["']invitation-room-title["']/i.test(String(t||""))}export function previewArtifactIssue(e,t){const r=new URL(normalizeRoute(e),"http://127.0.0.1"),o=e=>{if("string"!=typeof e||!e.trim())return null;try{const t=new URL(e,"http://127.0.0.1");return`${"/"===t.pathname?"/":t.pathname.replace(/\/+$/,"")}${t.search}`}catch{return null}},n=o(r.href),i="/code"===r.pathname||r.pathname.startsWith("/code/")||r.searchParams.has("r");for(const e of[t?.beforeState,t?.afterState]){if(!e)return"The captured page did not report settled rendered state.";const t=o(e.url);if(t&&t!==n)return`Preview navigated from ${n} to ${t}.`;if(e.signedOutInvite)return"Preview resolved to the signed-out room invitation instead of the authenticated Code room.";if(e.visibleBoot)return"Preview was still showing the application loading shell.";if(e.visibleLoading)return"Preview was still showing an application loading state.";if(i&&e.prerenderGeo)return"Preview resolved to the public marketing shell instead of the authenticated Code surface.";if(!e.hasBody||e.textLength<2&&e.meaningfulVisualCount<1)return"Preview rendered an empty page."}return null}export function routeNeedsPreviewAuth(e){const t=new URL(normalizeRoute(e),"http://127.0.0.1");return!!t.searchParams.get("r")||["/settings","/contacts","/code/link"].includes(t.pathname)}export function previewAuthBootstrap({accessToken:e,supabaseUrl:t,nowSec:r=Math.floor(Date.now()/1e3),minTtlSec:o=0}={}){const n=function(e){try{const t=String(e||"").split(".")[1];return t?JSON.parse(Buffer.from(t,"base64url").toString("utf8")):null}catch{return null}}(e);let i;try{i=new URL(t)}catch{return null}if(!e||"https:"!==i.protocol||!n?.sub||!n?.exp||n.exp-r<=o)return null;const s=i.hostname.split(".")[0];return s?{storageKey:`sb-${s}-auth-token`,allowedReadOrigins:[i.origin],allowedSocketOrigins:[`wss://${i.host}`],session:{access_token:e,refresh_token:"",expires_in:Math.max(1,n.exp-r)+3600,expires_at:n.exp+3600,token_type:"bearer",user:{id:n.sub,aud:n.aud||"authenticated",role:n.role||"authenticated",email:n.email||null,phone:n.phone||"",app_metadata:n.app_metadata||{},user_metadata:n.user_metadata||{},identities:[],created_at:n.created_at||new Date(1e3*r).toISOString(),updated_at:new Date(1e3*r).toISOString()}}}:null}export function findBrowserExecutable({env:e=process.env,platform:r=process.platform,exists:o=t.existsSync}={}){return[e.TP_BROWSER_PATH,"darwin"===r&&"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","darwin"===r&&"/Applications/Chromium.app/Contents/MacOS/Chromium","win32"===r&&e.PROGRAMFILES&&n.join(e.PROGRAMFILES,"Google","Chrome","Application","chrome.exe"),"win32"===r&&e["PROGRAMFILES(X86)"]&&n.join(e["PROGRAMFILES(X86)"],"Google","Chrome","Application","chrome.exe"),"linux"===r&&"/usr/bin/google-chrome","linux"===r&&"/usr/bin/google-chrome-stable","linux"===r&&"/usr/bin/chromium","linux"===r&&"/usr/bin/chromium-browser"].filter(Boolean).find(e=>{try{return o(e)}catch{return!1}})||null}class d{constructor(e,t=12e3){this.child=e,this.timeoutMs=t,this.nextId=1,this.buffer=Buffer.alloc(0),this.pending=new Map,this.waiters=new Set,this.subscribers=new Set,e.stdio[4].on("data",e=>this.onData(e)),e.once("exit",(e,t)=>this.failAll(new Error(`Chrome exited (${t||e||"unknown"}).`))),e.once("error",e=>this.failAll(e))}onData(e){for(this.buffer=Buffer.concat([this.buffer,e]);;){const e=this.buffer.indexOf(0);if(e<0)break;const t=this.buffer.subarray(0,e).toString("utf8");if(this.buffer=this.buffer.subarray(e+1),!t)continue;let r;try{r=JSON.parse(t)}catch{continue}if(r.id){const e=this.pending.get(r.id);if(!e)continue;clearTimeout(e.timer),this.pending.delete(r.id),r.error?e.reject(new Error(r.error.message||"Chrome DevTools error")):e.resolve(r.result||{});continue}for(const e of[...this.waiters])e.method===r.method&&(e.sessionId&&e.sessionId!==r.sessionId||e.predicate&&!e.predicate(r.params||{})||(clearTimeout(e.timer),this.waiters.delete(e),e.resolve(r.params||{})));for(const e of[...this.subscribers])if(e.method===r.method&&(!e.sessionId||e.sessionId===r.sessionId))try{e.handler(r.params||{})}catch{}}}failAll(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear();for(const t of this.waiters)clearTimeout(t.timer),t.reject(e);this.waiters.clear(),this.subscribers.clear()}send(e,t={},r){const o=this.nextId++;return new Promise((n,i)=>{const s=setTimeout(()=>{this.pending.delete(o),i(new Error(`Chrome timed out running ${e}.`))},this.timeoutMs);this.pending.set(o,{resolve:n,reject:i,timer:s});const a={id:o,method:e,params:t,...r?{sessionId:r}:{}};this.child.stdio[3].write(`${JSON.stringify(a)}\0`)})}waitFor(e,t,r){return new Promise((o,n)=>{const i={method:e,sessionId:t,predicate:r,resolve:o,reject:n,timer:null};i.timer=setTimeout(()=>{this.waiters.delete(i),n(new Error(`Chrome timed out waiting for ${e}.`))},this.timeoutMs),this.waiters.add(i)})}subscribe(e,t,r){const o={method:e,sessionId:t,handler:r};return this.subscribers.add(o),()=>this.subscribers.delete(o)}}export class CdpBrowser{constructor({executable:t,spawnImpl:r=e,timeoutMs:o=12e3}={}){this.executable=t||null,this.spawnImpl=r,this.timeoutMs=o,this.child=null,this.pipe=null,this.profileDir=null,this.starting=null,this.stderr=""}async ensureStarted(){if(!this.child||!this.pipe)return this.starting||(this.starting=this.start().finally(()=>{this.starting=null})),this.starting}async start(){const e=this.executable||findBrowserExecutable();if(!e)throw new Error("No supported Chrome/Chromium executable found. Install Google Chrome or set TP_BROWSER_PATH on the bridge host.");this.profileDir=await r.mkdtemp(n.join(o.tmpdir(),"thinkpool-viewport-"));const t=["--headless=new","--remote-debugging-pipe","--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-extensions","--disable-default-apps","--metrics-recording-only","--mute-audio","--hide-scrollbars",`--user-data-dir=${this.profileDir}`,"about:blank"],i=this.spawnImpl(e,t,{stdio:["ignore","ignore","pipe","pipe","pipe"]});this.child=i,i.stderr?.on("data",e=>{this.stderr=(this.stderr+e.toString("utf8")).slice(-4e3)}),this.pipe=new d(i,this.timeoutMs);try{await this.pipe.send("Browser.getVersion")}catch(e){await this.close();const t=this.stderr.trim().split("\n").slice(-2).join(" ");throw new Error(`Could not start Chrome for viewport capture: ${t||e.message}`)}}async withPage({url:e,viewport:t,waitMs:r=300,auth:o=null},n){await this.ensureStarted();const{targetId:i}=await this.pipe.send("Target.createTarget",{url:"about:blank"}),{sessionId:s}=await this.pipe.send("Target.attachToTarget",{targetId:i,flatten:!0});let a=null,l=null,c=null;const h=[],d=[],p=[];try{if(await this.pipe.send("Page.enable",{},s),await this.pipe.send("Runtime.enable",{},s),await this.pipe.send("Network.enable",{},s).catch(()=>{}),o?.storageKey&&o?.session){const e=`(() => { try { localStorage.setItem(${JSON.stringify(o.storageKey)}, ${JSON.stringify(JSON.stringify(o.session))}); window.__TP_READ_ONLY_PREVIEW__ = true; } catch {} })()`;await this.pipe.send("Page.addScriptToEvaluateOnNewDocument",{source:e},s)}const i=new URL(e).origin,w=new Set(o?.allowedReadOrigins||[]),m=new Set(o?.allowedSocketOrigins||[]);l=this.pipe.subscribe("Network.responseReceived",s,e=>{try{const t=new URL(e.response?.url||"");w.has(t.origin)&&h.length<20&&h.push(`${e.response?.status||0}:${t.pathname}`),t.origin===i&&d.length<20&&d.push(`${e.response?.status||0}:${t.pathname}`)}catch{}}),c=this.pipe.subscribe("Runtime.exceptionThrown",s,e=>{if(p.length>=5)return;const t=e.exceptionDetails?.exception?.description||e.exceptionDetails?.text||"runtime error";p.push(String(t).split("\n")[0].slice(0,180))}),a=this.pipe.subscribe("Fetch.requestPaused",s,e=>{let t=!1;try{const r=new URL(e.request?.url||""),o=String(e.request?.method||"GET").toUpperCase();t=r.origin===i||w.has(r.origin)&&["GET","HEAD","OPTIONS"].includes(o)||m.has(r.origin)&&"GET"===o}catch{t=!1}const r=t?"Fetch.continueRequest":"Fetch.failRequest",o=t?{requestId:e.requestId}:{requestId:e.requestId,errorReason:"BlockedByClient"};this.pipe.send(r,o,s).catch(()=>{})}),await this.pipe.send("Fetch.enable",{patterns:[{urlPattern:"*",requestStage:"Request"}]},s),await this.pipe.send("Network.setBypassServiceWorker",{bypass:!0},s).catch(()=>{}),await this.pipe.send("Emulation.setDeviceMetricsOverride",{width:t.width,height:t.height,deviceScaleFactor:1,mobile:!1,screenWidth:t.width,screenHeight:t.height},s);const f=this.pipe.waitFor("Page.loadEventFired",s),g=await this.pipe.send("Page.navigate",{url:e},s);if(g.errorText)throw new Error(`Preview navigation failed: ${g.errorText}`);if(await f,await this.pipe.send("Runtime.evaluate",{expression:"document.fonts && document.fonts.ready",awaitPromise:!0,returnByValue:!0},s).catch(()=>{}),o){const e=Date.now()+15e3;let t=!1,r=0;for(;Date.now()<e;){const e=await this.pipe.send("Runtime.evaluate",{expression:"(() => {\n if (document.querySelector('#invitation-room-title')) return true;\n if (document.querySelector('#boot, .boot-loader')) return false;\n const room = document.querySelector('[data-tp-preview-room-status]');\n if (room?.dataset.tpPreviewRoomStatus === 'pending') return false;\n const loading = [...document.querySelectorAll('span')].some((node) => /^(loading session|restoring terminals…|connecting(?:…|...))$/i.test((node.textContent || '').trim()));\n return !loading && (document.body?.innerText || '').trim().length > 1;\n })()",returnByValue:!0},s);if(!0===e.result?.value){if(r||(r=Date.now()),Date.now()-r>=800){t=!0;break}}else r=0;await new Promise(e=>setTimeout(e,100))}if(!t){const e=await this.pipe.send("Runtime.evaluate",{expression:`(async () => {\n let locks = { held: [], pending: [] };\n try { locks = await navigator.locks.query(); } catch {}\n return {\n sessionPresent: !!localStorage.getItem(${JSON.stringify(o.storageKey||"")}),\n inlineBoot: !!document.querySelector('#boot, .boot-loader'),\n loadingSession: [...document.querySelectorAll('span')].some((node) => (node.textContent || '').trim() === 'Loading session'),\n connecting: [...document.querySelectorAll('span')].some((node) => (node.textContent || '').trim().toLowerCase() === 'connecting…'),\n invitation: !!document.querySelector('#invitation-room-title'),\n roomStatus: document.querySelector('[data-tp-preview-room-status]')?.dataset.tpPreviewRoomStatus || null,\n heldLocks: locks.held?.length || 0,\n pendingLocks: locks.pending?.length || 0,\n };\n })()`,awaitPromise:!0,returnByValue:!0},s).catch(()=>({result:{value:{}}})),t=e.result?.value||{};throw new Error(`Authenticated preview did not leave its loading state within 15 seconds (session=${t.sessionPresent?"present":"missing"}, inlineBoot=${t.inlineBoot?"yes":"no"}, loadingSession=${t.loadingSession?"yes":"no"}, connecting=${t.connecting?"yes":"no"}, roomStatus=${t.roomStatus||"none"}, authLocks=${t.heldLocks||0}/${t.pendingLocks||0}, invitation=${t.invitation?"yes":"no"}, reads=${h.join(",")||"none"}, local=${d.join(",")||"none"}, runtime=${p.join(" | ")||"none"}); no screenshot was created.`)}}const b=o?Math.min(300,Math.max(0,Number(r)||0)):Math.min(u,Math.max(0,Number(r)||0));return b&&await new Promise(e=>setTimeout(e,b)),await n({pipe:this.pipe,sessionId:s})}finally{a?.(),l?.(),c?.(),await this.pipe.send("Target.closeTarget",{targetId:i}).catch(()=>{})}}async renderedState(e,t){const r=await e.send("Runtime.evaluate",{expression:"(() => {\n const visible = (node) => {\n if (!node) return false;\n const style = getComputedStyle(node);\n const rect = node.getBoundingClientRect();\n return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0 && rect.width > 0 && rect.height > 0;\n };\n const boot = document.querySelector('#boot');\n const meaningful = [...document.querySelectorAll('main,article,section,img,video,canvas,svg,[role=\"dialog\"],[role=\"main\"],[role=\"img\"]')]\n .filter((node) => !node.closest('#boot') && visible(node));\n return {\n title: document.title || '', url: location.href,\n hasBody: !!document.body,\n textLength: (document.body?.innerText || '').trim().length,\n meaningfulVisualCount: meaningful.length,\n visibleBoot: visible(boot),\n visibleLoading: [...document.querySelectorAll('span')].some((node) => {\n const text = (node.textContent || '').trim();\n return visible(node) && /^(loading session|restoring terminals…|connecting(?:…|...))$/i.test(text);\n }),\n signedOutInvite: !!document.querySelector('#invitation-room-title'),\n prerenderGeo: !!document.querySelector('[data-prerender=\"geo\"]'),\n };\n })()",returnByValue:!0},t);if(r.exceptionDetails)throw new Error("Could not inspect the rendered preview state.");return r.result?.value||null}async freezeCurrentPage(e,t){const r=await e.send("Runtime.evaluate",{expression:"(async () => {\n const clone = document.documentElement.cloneNode(true);\n clone.querySelectorAll('script,link[rel=\"modulepreload\"],link[rel=\"preload\"]').forEach((node) => node.remove());\n clone.querySelectorAll('*').forEach((node) => {\n for (const attr of [...node.attributes]) if (/^on/i.test(attr.name)) node.removeAttribute(attr.name);\n if (node instanceof HTMLInputElement) node.removeAttribute('value');\n if (node instanceof HTMLTextAreaElement) node.textContent = '';\n });\n const css = [];\n for (const sheet of [...document.styleSheets]) {\n try { css.push([...sheet.cssRules].map((rule) => rule.cssText).join('\\n')); } catch {}\n }\n clone.querySelectorAll('style,link[rel=\"stylesheet\"]').forEach((node) => node.remove());\n const head = clone.querySelector('head') || clone.insertBefore(document.createElement('head'), clone.firstChild);\n const style = document.createElement('style');\n style.setAttribute('data-thinkpool-snapshot', '');\n style.textContent = css.join('\\n');\n head.appendChild(style);\n\n const assetUrls = new Set();\n const remember = (raw) => {\n if (!raw || /^(data:|blob:|#)/i.test(raw)) return;\n try {\n const absolute = new URL(raw, location.href);\n if (absolute.origin === location.origin) assetUrls.add(absolute.href);\n } catch {}\n };\n clone.querySelectorAll('[src],[poster]').forEach((node) => {\n remember(node.getAttribute('src'));\n remember(node.getAttribute('poster'));\n });\n for (const match of style.textContent.matchAll(/url\\(\\s*(['\"]?)([^'\"\\)]+)\\1\\s*\\)/gi)) remember(match[2]);\n\n const replacements = new Map();\n let inlinedBytes = 0;\n for (const absolute of assetUrls) {\n try {\n const response = await fetch(absolute);\n const buffer = await response.arrayBuffer();\n if (!response.ok || buffer.byteLength > 512000 || inlinedBytes + buffer.byteLength > 1000000) continue;\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));\n replacements.set(absolute, 'data:' + (response.headers.get('content-type') || 'application/octet-stream') + ';base64,' + btoa(binary));\n inlinedBytes += buffer.byteLength;\n } catch {}\n }\n const replaceAsset = (raw) => {\n try { return replacements.get(new URL(raw, location.href).href) || raw; } catch { return raw; }\n };\n clone.querySelectorAll('[src],[poster]').forEach((node) => {\n for (const name of ['src', 'poster']) if (node.hasAttribute(name)) node.setAttribute(name, replaceAsset(node.getAttribute(name)));\n node.removeAttribute('srcset');\n });\n style.textContent = style.textContent.replace(/url\\(\\s*(['\"]?)([^'\"\\)]+)\\1\\s*\\)/gi, (_all, quote, raw) => 'url(\"' + replaceAsset(raw) + '\")');\n return '<!doctype html>\\n' + clone.outerHTML;\n })()",awaitPromise:!0,returnByValue:!0},t);if(r.exceptionDetails||"string"!=typeof r.result?.value){const e=r.exceptionDetails?.exception?.description||r.exceptionDetails?.text||r.result?.description;throw new Error("Could not freeze the rendered preview for Design"+(e?`: ${e}`:"."))}const o=r.result.value;if(!o.trim()||Buffer.byteLength(o)>19e5)throw new Error("The rendered preview is too large to make editable.");return o}async capture({url:e,viewport:t,fullPage:r=!0,waitMs:o=300,includeSnapshot:n=!1,auth:i=null}){return this.withPage({url:e,viewport:t,waitMs:o,auth:i},async({pipe:e,sessionId:o})=>{const i=await this.renderedState(e,o),s=await e.send("Page.getLayoutMetrics",{},o),a=Math.ceil(s.cssContentSize?.height||t.height),l=r?Math.min(2e4,Math.max(t.height,a)):t.height,c=await e.send("Page.captureScreenshot",{format:"png",fromSurface:!0,captureBeyondViewport:!0,clip:{x:0,y:0,width:t.width,height:l,scale:1}},o),u=await this.renderedState(e,o),h=n?await this.freezeCurrentPage(e,o):null;return{png:Buffer.from(c.data,"base64"),width:t.width,height:l,contentHeight:a,capped:a>2e4,beforeState:i,afterState:u,snapshot:h}})}async snapshot({url:e,viewport:t=DEFAULT_VIEWPORTS.desktop,waitMs:r=300,auth:o=null}){return this.withPage({url:e,viewport:t,waitMs:r,auth:o},({pipe:e,sessionId:t})=>this.freezeCurrentPage(e,t))}async inspect({url:e,viewport:t=DEFAULT_VIEWPORTS.mobile,selector:r,waitMs:o=300,auth:n=null}){return this.withPage({url:e,viewport:t,waitMs:o,auth:n},async({pipe:e,sessionId:t})=>{const o=`(() => {\n const selector = ${JSON.stringify(r||null)};\n const el = selector ? document.querySelector(selector) : null;\n const r = el && el.getBoundingClientRect();\n return {\n title: document.title, url: location.href,\n viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio },\n document: { width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth || 0), height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0) },\n selector: selector ? { query: selector, found: !!el, tag: el?.tagName || null, text: el?.innerText?.slice(0, 2000) || '', rect: r ? { x:r.x, y:r.y, width:r.width, height:r.height } : null } : null,\n text: document.body?.innerText?.slice(0, 12000) || ''\n };\n })()`,n=await e.send("Runtime.evaluate",{expression:o,returnByValue:!0},t);if(n.exceptionDetails)throw new Error("DOM inspection failed in the preview page.");return n.result?.value||{}})}async close(){const e=this.child,t=this.pipe;if(this.child=null,this.pipe=null,t&&await t.send("Browser.close").catch(()=>{}),e&&null==e.exitCode)try{e.kill("SIGTERM")}catch{}this.profileDir&&await r.rm(this.profileDir,{recursive:!0,force:!0}).catch(()=>{}),this.profileDir=null}}export const sharedViewportBrowser=new CdpBrowser;export class ViewportManager{constructor({workspaceRoot:e,ownerId:t,roomCode:r,outbox:s,browser:l=sharedViewportBrowser,startPreviewImpl:c=a,designContext:u=null,authContext:h=null}={}){this.workspaceRoot=n.resolve(e||process.cwd()),this.ownerId=t||i(),this.roomCode="string"==typeof r&&r.trim()?r.trim():null,this.outbox=s||n.join(o.tmpdir(),"thinkpool-viewport-captures",this.ownerId),this.browser=l,this.startPreviewImpl=c,this.designContext="function"==typeof u?u:()=>null,this.authContext="function"==typeof h?h:()=>null,this.previewId=`viewport:${this.ownerId}`,this.preview=null,this.root=null}setWorkspaceRoot(e){if(!e)throw new Error("A lane workspace is required.");return this.workspaceRoot=n.resolve(e),this.workspaceRoot}async start({root:e="dist"}={}){const t=await resolveContainedRoot(this.workspaceRoot,e);await this.stop();const r=await this.startPreviewImpl({dir:t,host:"127.0.0.1",port:0,id:this.previewId}),o=new URL(r.url);if("127.0.0.1"!==o.hostname&&"localhost"!==o.hostname)throw await(r.stop?.()),new Error("Preview server did not bind to loopback.");return this.preview=r,this.root=t,{url:r.url,root:t}}requirePreview(){if(!this.preview?.url)throw new Error("No preview is running. Call preview_start after building the app.");return this.preview}defaultRoute(){return this.roomCode?`/?r=${encodeURIComponent(this.roomCode)}`:"/"}resolveRoute(e){return normalizeRoute(e??this.defaultRoute())}pageUrl(e){const t=this.requirePreview();return new URL(normalizeRoute(e),t.url).href}async previewAuth(e){if(!routeNeedsPreviewAuth(e))return null;let t=this.authContext()||{};if(!t.accessToken)return null;const r=Date.now()+15e3;for(;;){const e=previewAuthBootstrap({...t,minTtlSec:30});if(e)return e;if(Date.now()>=r)throw new Error("The bridge account token did not refresh in time for an authenticated preview; no screenshot was created.");await new Promise(e=>setTimeout(e,250)),t=this.authContext()||{}}}async capture({route:e,viewports:t="both",title:o="Viewport capture",fullPage:i=!0,waitMs:s=300,card:a=!1}={}){const l=this.resolveRoute(e),c=this.pageUrl(l),u="both"===t?["desktop","mobile"]:[t];if(u.some(e=>!DEFAULT_VIEWPORTS[e]))throw new Error('viewports must be "both", "desktop", or "mobile".');const h=`${safeSlug(o)}-${Date.now().toString(36)}`,d=await r.realpath(this.workspaceRoot),p=n.relative(d,this.root)||".",w=JSON.stringify([p,l]),m=this.designContext()||null,f=m?.captureKey===w?{designRequestId:m.requestId,parentRevision:m.parentRevision}:{},g=!0===a||!!f.designRequestId,b={};for(const e of u)b[e]=await this.browser.capture({url:c,viewport:DEFAULT_VIEWPORTS[e],fullPage:i,waitMs:s,includeSnapshot:g&&e===(u.includes("desktop")?"desktop":u[0]),auth:await this.previewAuth(l)});const v=g&&!(!b.desktop||!b.mobile);if(v||routeNeedsPreviewAuth(l))for(const[e,t]of Object.entries(b)){const r=previewArtifactIssue(l,t);if(r)throw new Error(`${e} preview rejected: ${r} No screenshot or mockup card was created.`)}const y=b.desktop?.snapshot||b.mobile?.snapshot||null;if(v&&("string"!=typeof y||!y.trim()))throw new Error("The deliverable could not freeze its rendered source. No mockup card was created.");await r.mkdir(this.outbox,{recursive:!0});for(const[e,t]of Object.entries(b))t.file=n.join(this.outbox,`${h}--${e}.png`),await r.writeFile(t.file,t.png);const x=v?n.join(this.outbox,`${h}--source.html`):null;x&&await r.writeFile(x,y);let S=null;if(v){const e=await resolvePreviewSourceMap(this.root),t={slug:h,title:String(o||"Viewport capture").slice(0,120),desktop:b.desktop.file,mobile:b.mobile.file,ts:Date.now(),sourceKind:"preview",deliveryIntent:"card",readinessValidated:!0,snapshot:x,previewRoot:p,route:l,captureKey:w,...e?{sourceMap:e}:{},...f};S=n.join(this.outbox,`${h}.json`);const i=`${S}.tmp`;await r.writeFile(i,JSON.stringify(t)),await r.rename(i,S)}return{url:c,route:l,slug:h,manifestPath:S,captures:b,snapshotPath:x,cardReady:v}}async inspect({route:e,viewport:t="mobile",selector:r,waitMs:o=300}={}){if(!DEFAULT_VIEWPORTS[t])throw new Error('viewport must be "desktop" or "mobile".');const n=this.resolveRoute(e);return this.browser.inspect({url:this.pageUrl(n),viewport:DEFAULT_VIEWPORTS[t],selector:r,waitMs:o,auth:await this.previewAuth(n)})}async stop(){const e=this.preview||s.get(this.previewId);this.preview=null,this.root=null,e?.stop&&await e.stop()}}const p=e=>({content:[{type:"text",text:e}]}),w=e=>p(`Viewport error: ${e?.message||e}`);export function createViewportTools({tool:e,z:t,manager:r}){return[e("preview_start","Start a bridge-owned, read-only localhost preview for built files inside this lane workspace. Run the project build first. The default root is dist; pass another relative directory only when its index.html is the intended preview.",{root:t.string().max(240).optional().describe("relative directory inside this lane workspace; default: dist")},async e=>{try{const t=await r.start({root:e?.root||"dist"});return p(`Preview ready from ${n.relative(r.workspaceRoot,t.root)||"."}. Use preview_capture to see desktop/mobile, or preview_inspect for DOM text and geometry.`)}catch(e){return w(e)}}),e("preview_capture","Capture this lane preview in a fresh isolated browser at exact desktop (1440×900) and mobile (390×844) CSS viewports. Omit path to preview the current authenticated room; pass / explicitly for the public landing page. Defaults to both and full-page. Captures are inline verification evidence by default. Room and protected routes automatically use the bridge account as a read-only authenticated visual harness; external REST writes, presence, and room broadcasts are blocked, apart from non-mutating roster/transcript snapshot requests. Set card=true only for a user-requested mockup or visual deliverable that should remain editable in the transcript; complete desktop+mobile settled content is then required and the card arrives after the final response.",{path:t.string().max(500).optional().describe("route within the preview; omit for the current authenticated room, or pass / for the public landing page; never a full URL"),viewports:t.enum(["both","desktop","mobile"]).optional(),title:t.string().max(120).optional(),fullPage:t.boolean().optional(),card:t.boolean().optional().describe("default false; true only for an intentional user-facing mockup/Design deliverable"),waitMs:t.number().int().min(0).max(u).optional().describe("settle time after load, max 5000ms")},async e=>{try{const t=await r.capture({route:e?.path,viewports:e?.viewports||"both",title:e?.title||"Viewport capture",fullPage:!1!==e?.fullPage,waitMs:e?.waitMs??300,card:!0===e?.card}),o=t.cardReady?"The complete room preview card is queued after your final response.":"This is verification evidence only; no room card was created.",n=[{type:"text",text:`Captured ${Object.keys(t.captures).join(" + ")} for ${t.route}. ${o}`}];for(const[e,r]of Object.entries(t.captures))n.push({type:"text",text:`${e}: ${r.width}×${r.height}${r.capped?" (height capped)":""}`}),n.push({type:"image",data:r.png.toString("base64"),mimeType:"image/png"});return{content:n}}catch(e){return w(e)}}),e("preview_inspect","Inspect rendered DOM state in this lane preview at an exact desktop or mobile viewport. Omit path for the current authenticated room; pass / explicitly for the public landing page. Returns page text, document dimensions, and optional selector text/geometry without changing the page.",{path:t.string().max(500).optional().describe("route within the preview; omit for the current authenticated room, or pass / for the public landing page; never a full URL"),viewport:t.enum(["desktop","mobile"]).optional(),selector:t.string().max(500).optional(),waitMs:t.number().int().min(0).max(u).optional()},async e=>{try{const t=await r.inspect({route:e?.path,viewport:e?.viewport||"mobile",selector:e?.selector,waitMs:e?.waitMs??300});return p(JSON.stringify(t,null,2))}catch(e){return w(e)}}),e("preview_stop","Stop this lane's bridge-owned preview server and release its port.",{},async()=>{try{return await r.stop(),p("Preview stopped.")}catch(e){return w(e)}})]}
1
+ import{spawn as e}from"node:child_process";import t from"node:fs";import r from"node:fs/promises";import o from"node:os";import i from"node:path";import{randomUUID as n}from"node:crypto";import{acquireBrowserJob as s}from"./browser-job.mjs";import{previews as a,startPreview as l}from"./flow-preview.mjs";import{DESIGN_SOURCE_MAP_RELATIVE as c,MAX_DESIGN_SOURCE_MAP_BYTES as u}from"./design-source-contract.mjs";export const DEFAULT_VIEWPORTS=Object.freeze({desktop:Object.freeze({width:1440,height:900}),mobile:Object.freeze({width:390,height:844,mobile:!0,hasTouch:!0})});const d=5e3;export const BROWSER_IDLE_TIMEOUT_MS=6e4;const h=(e,t)=>{const r=i.relative(e,t);return""===r||!r.startsWith("..")&&!i.isAbsolute(r)};export async function resolveContainedRoot(e,o="dist"){if(!e)throw new Error("A lane workspace is required.");if(i.isAbsolute(o))throw new Error("Preview root must be relative to the lane workspace.");const n=await r.realpath(i.resolve(e)),s=i.resolve(n,o||"dist");let a;try{a=await r.realpath(s)}catch{throw new Error(`Preview root does not exist: ${o||"dist"}. Build the app first, or choose a directory containing index.html.`)}if(!h(n,a))throw new Error("Preview root escapes the lane workspace.");if(!(await r.stat(a)).isDirectory())throw new Error("Preview root must be a directory.");if(!t.existsSync(i.join(a,"index.html")))throw new Error(`No index.html found in preview root: ${o||"dist"}.`);return a}export async function resolvePreviewSourceMap(e){if(!e)return null;let t,o;try{t=await r.realpath(e),o=await r.realpath(i.join(t,c));const n=await r.stat(o);if(!n.isFile()||n.size<=0||n.size>u)return null}catch{return null}return h(t,o)?o:null}export function normalizeRoute(e="/"){const t=String(e||"/").trim();if(!t.startsWith("/")||t.startsWith("//"))throw new Error('Preview path must start with one "/" and cannot be a URL.');const r=new URL(t,"http://127.0.0.1");if("http://127.0.0.1"!==r.origin)throw new Error("Preview path cannot change the preview origin.");return`${r.pathname}${r.search}${r.hash}`}export function safeSlug(e="viewport"){return String(e||"viewport").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,56)||"viewport"}export function isSignedOutRoomPreview(e,t){return!!new URL(normalizeRoute(e),"http://127.0.0.1").searchParams.get("r")&&/\bid\s*=\s*["']invitation-room-title["']/i.test(String(t||""))}export function previewArtifactIssue(e,t){const r=new URL(normalizeRoute(e),"http://127.0.0.1"),o=e=>{if("string"!=typeof e||!e.trim())return null;try{const t=new URL(e,"http://127.0.0.1");return`${"/"===t.pathname?"/":t.pathname.replace(/\/+$/,"")}${t.search}`}catch{return null}},i=o(r.href),n="/code"===r.pathname||r.pathname.startsWith("/code/")||r.searchParams.has("r");for(const e of[t?.beforeState,t?.afterState]){if(!e)return"The captured page did not report settled rendered state.";const t=o(e.url);if(t&&t!==i)return`Preview navigated from ${i} to ${t}.`;if(e.signedOutInvite)return"Preview resolved to the signed-out room invitation instead of the authenticated Code room.";if(e.visibleBoot)return"Preview was still showing the application loading shell.";if(e.visibleLoading)return"Preview was still showing an application loading state.";if(n&&e.prerenderGeo)return"Preview resolved to the public marketing shell instead of the authenticated Code surface.";if(!e.hasBody||e.textLength<2&&e.meaningfulVisualCount<1)return"Preview rendered an empty page."}return null}export function routeNeedsPreviewAuth(e){const t=new URL(normalizeRoute(e),"http://127.0.0.1");return!!t.searchParams.get("r")||["/settings","/contacts","/code/link"].includes(t.pathname)}export function previewAuthBootstrap({accessToken:e,supabaseUrl:t,nowSec:r=Math.floor(Date.now()/1e3),minTtlSec:o=0}={}){const i=function(e){try{const t=String(e||"").split(".")[1];return t?JSON.parse(Buffer.from(t,"base64url").toString("utf8")):null}catch{return null}}(e);let n;try{n=new URL(t)}catch{return null}if(!e||"https:"!==n.protocol||!i?.sub||!i?.exp||i.exp-r<=o)return null;const s=n.hostname.split(".")[0];return s?{storageKey:`sb-${s}-auth-token`,allowedReadOrigins:[n.origin],allowedSocketOrigins:[`wss://${n.host}`],session:{access_token:e,refresh_token:"",expires_in:Math.max(1,i.exp-r)+3600,expires_at:i.exp+3600,token_type:"bearer",user:{id:i.sub,aud:i.aud||"authenticated",role:i.role||"authenticated",email:i.email||null,phone:i.phone||"",app_metadata:i.app_metadata||{},user_metadata:i.user_metadata||{},identities:[],created_at:i.created_at||new Date(1e3*r).toISOString(),updated_at:new Date(1e3*r).toISOString()}}}:null}export function findBrowserExecutable({env:e=process.env,platform:r=process.platform,exists:o=t.existsSync}={}){return[e.TP_BROWSER_PATH,"darwin"===r&&"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","darwin"===r&&"/Applications/Chromium.app/Contents/MacOS/Chromium","win32"===r&&e.PROGRAMFILES&&i.join(e.PROGRAMFILES,"Google","Chrome","Application","chrome.exe"),"win32"===r&&e["PROGRAMFILES(X86)"]&&i.join(e["PROGRAMFILES(X86)"],"Google","Chrome","Application","chrome.exe"),"linux"===r&&"/usr/bin/google-chrome","linux"===r&&"/usr/bin/google-chrome-stable","linux"===r&&"/usr/bin/chromium","linux"===r&&"/usr/bin/chromium-browser"].filter(Boolean).find(e=>{try{return o(e)}catch{return!1}})||null}class p{constructor(e,t=12e3){this.child=e,this.timeoutMs=t,this.nextId=1,this.buffer=Buffer.alloc(0),this.pending=new Map,this.waiters=new Set,this.subscribers=new Set,e.stdio[4].on("data",e=>this.onData(e)),e.once("exit",(e,t)=>this.failAll(new Error(`Chrome exited (${t||e||"unknown"}).`))),e.once("error",e=>this.failAll(e))}onData(e){for(this.buffer=Buffer.concat([this.buffer,e]);;){const e=this.buffer.indexOf(0);if(e<0)break;const t=this.buffer.subarray(0,e).toString("utf8");if(this.buffer=this.buffer.subarray(e+1),!t)continue;let r;try{r=JSON.parse(t)}catch{continue}if(r.id){const e=this.pending.get(r.id);if(!e)continue;clearTimeout(e.timer),this.pending.delete(r.id),r.error?e.reject(new Error(r.error.message||"Chrome DevTools error")):e.resolve(r.result||{});continue}for(const e of[...this.waiters])e.method===r.method&&(e.sessionId&&e.sessionId!==r.sessionId||e.predicate&&!e.predicate(r.params||{})||(clearTimeout(e.timer),this.waiters.delete(e),e.resolve(r.params||{})));for(const e of[...this.subscribers])if(e.method===r.method&&(!e.sessionId||e.sessionId===r.sessionId))try{e.handler(r.params||{})}catch{}}}failAll(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear();for(const t of this.waiters)clearTimeout(t.timer),t.reject(e);this.waiters.clear(),this.subscribers.clear()}send(e,t={},r){const o=this.nextId++;return new Promise((i,n)=>{const s=setTimeout(()=>{this.pending.delete(o),n(new Error(`Chrome timed out running ${e}.`))},this.timeoutMs);this.pending.set(o,{resolve:i,reject:n,timer:s});const a={id:o,method:e,params:t,...r?{sessionId:r}:{}};this.child.stdio[3].write(`${JSON.stringify(a)}\0`)})}waitFor(e,t,r){return new Promise((o,i)=>{const n={method:e,sessionId:t,predicate:r,resolve:o,reject:i,timer:null};n.timer=setTimeout(()=>{this.waiters.delete(n),i(new Error(`Chrome timed out waiting for ${e}.`))},this.timeoutMs),this.waiters.add(n)})}subscribe(e,t,r){const o={method:e,sessionId:t,handler:r};return this.subscribers.add(o),()=>this.subscribers.delete(o)}cancelWaiters(e){for(const t of[...this.waiters])t.sessionId===e&&(clearTimeout(t.timer),this.waiters.delete(t),t.reject(new Error("Preview page closed.")))}}export class CdpBrowser{constructor({executable:t,spawnImpl:r=e,timeoutMs:o=12e3,idleTimeoutMs:i=6e4,acquireJob:n=s}={}){this.executable=t||null,this.spawnImpl=r,this.timeoutMs=o,this.child=null,this.pipe=null,this.profileDir=null,this.starting=null,this.closing=null,this.idleTimer=null,this.idleTimeoutMs=i,this.acquireJob=n,this.jobs=0,this.version=null,this.stderr=""}async ensureStarted(){return this.closing&&await this.closing,this.starting?this.starting:(!this.child||null==this.child.exitCode&&null==this.child.signalCode||await this.close(),this.child&&this.pipe?void 0:(this.starting=this.start().finally(()=>{this.starting=null}),this.starting))}async start(){const e=this.executable||findBrowserExecutable();if(!e)throw new Error("No supported Chrome/Chromium executable found. Install Google Chrome or set TP_BROWSER_PATH on the bridge host.");this.profileDir=await r.mkdtemp(i.join(o.tmpdir(),"thinkpool-viewport-"));const t=["--headless=new","--remote-debugging-pipe","--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-extensions","--disable-default-apps","--metrics-recording-only","--mute-audio","--hide-scrollbars",`--user-data-dir=${this.profileDir}`,"about:blank"],n=this.spawnImpl(e,t,{stdio:["ignore","ignore","pipe","pipe","pipe"]});this.child=n,n.stderr?.on("data",e=>{this.stderr=(this.stderr+e.toString("utf8")).slice(-4e3)}),this.pipe=new p(n,this.timeoutMs);try{this.version=await this.pipe.send("Browser.getVersion")}catch(e){await this.close();const t=this.stderr.trim().split("\n").slice(-2).join(" ");throw new Error(`Could not start Chrome for viewport capture: ${t||e.message}`)}}async withPage({url:e,viewport:t,waitMs:r=300,auth:o=null},i){this.jobs++,clearTimeout(this.idleTimer),this.idleTimer=null;let n=null,s=null,a=null,l=null,c=null,u=null,h=null;const p=[],w=[],m=[];try{if(n=await this.acquireJob(),await this.ensureStarted(),s=this.pipe,a=(await s.send("Target.createBrowserContext",{disposeOnDetach:!0})).browserContextId,!a)throw new Error("Chrome did not create an isolated preview context.");const{targetId:f}=await s.send("Target.createTarget",{url:"about:blank",browserContextId:a}),g=await s.send("Target.attachToTarget",{targetId:f,flatten:!0});if(l=g.sessionId,await s.send("Page.enable",{},l),await s.send("Runtime.enable",{},l),await s.send("Network.enable",{},l).catch(()=>{}),o?.storageKey&&o?.session){const e=`(() => { try { localStorage.setItem(${JSON.stringify(o.storageKey)}, ${JSON.stringify(JSON.stringify(o.session))}); window.__TP_READ_ONLY_PREVIEW__ = true; } catch {} })()`;await s.send("Page.addScriptToEvaluateOnNewDocument",{source:e},l)}const b=new URL(e).origin,v=new Set(o?.allowedReadOrigins||[]),y=new Set(o?.allowedSocketOrigins||[]);if(u=s.subscribe("Network.responseReceived",l,e=>{try{const t=new URL(e.response?.url||"");v.has(t.origin)&&p.length<20&&p.push(`${e.response?.status||0}:${t.pathname}`),t.origin===b&&w.length<20&&w.push(`${e.response?.status||0}:${t.pathname}`)}catch{}}),h=s.subscribe("Runtime.exceptionThrown",l,e=>{if(m.length>=5)return;const t=e.exceptionDetails?.exception?.description||e.exceptionDetails?.text||"runtime error";m.push(String(t).split("\n")[0].slice(0,180))}),c=s.subscribe("Fetch.requestPaused",l,e=>{let t=!1;try{const r=new URL(e.request?.url||""),o=String(e.request?.method||"GET").toUpperCase();t=r.origin===b||v.has(r.origin)&&["GET","HEAD","OPTIONS"].includes(o)||y.has(r.origin)&&"GET"===o}catch{t=!1}const r=t?"Fetch.continueRequest":"Fetch.failRequest",o=t?{requestId:e.requestId}:{requestId:e.requestId,errorReason:"BlockedByClient"};s.send(r,o,l).catch(()=>{})}),await s.send("Fetch.enable",{patterns:[{urlPattern:"*",requestStage:"Request"}]},l),await s.send("Network.setBypassServiceWorker",{bypass:!0},l).catch(()=>{}),await s.send("Emulation.setDeviceMetricsOverride",{width:t.width,height:t.height,deviceScaleFactor:1,mobile:!0===t.mobile,screenWidth:t.width,screenHeight:t.height},l),await s.send("Emulation.setTouchEmulationEnabled",{enabled:!0===t.hasTouch,maxTouchPoints:t.hasTouch?5:1},l),t.mobile){const e=this.version?.product?.match(/(?:Chrome|Chromium)\/([\d.]+)/)?.[1];if(!e)throw new Error("Chrome did not report its version for mobile emulation.");await s.send("Emulation.setUserAgentOverride",{userAgent:`Mozilla/5.0 (Linux; Android 13; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${e} Mobile Safari/537.36`,platform:"Android",userAgentMetadata:{platform:"Android",platformVersion:"13.0.0",architecture:"",model:"",mobile:!0}},l)}const x=s.waitFor("Page.loadEventFired",l);x.catch(()=>{});const S=await s.send("Page.navigate",{url:e},l);if(S.errorText)throw new Error(`Preview navigation failed: ${S.errorText}`);if(await x,await s.send("Runtime.evaluate",{expression:"document.fonts && document.fonts.ready",awaitPromise:!0,returnByValue:!0},l).catch(()=>{}),o){const e=Date.now()+15e3;let t=!1,r=0;for(;Date.now()<e;){const e=await s.send("Runtime.evaluate",{expression:"(() => {\n if (document.querySelector('#invitation-room-title')) return true;\n if (document.querySelector('#boot, .boot-loader')) return false;\n const room = document.querySelector('[data-tp-preview-room-status]');\n if (room?.dataset.tpPreviewRoomStatus === 'pending') return false;\n const loading = [...document.querySelectorAll('span')].some((node) => /^(loading session|restoring terminals…|connecting(?:…|...))$/i.test((node.textContent || '').trim()));\n return !loading && (document.body?.innerText || '').trim().length > 1;\n })()",returnByValue:!0},l);if(!0===e.result?.value){if(r||(r=Date.now()),Date.now()-r>=800){t=!0;break}}else r=0;await new Promise(e=>setTimeout(e,100))}if(!t){const e=await s.send("Runtime.evaluate",{expression:`(async () => {\n let locks = { held: [], pending: [] };\n try { locks = await navigator.locks.query(); } catch {}\n return {\n sessionPresent: !!localStorage.getItem(${JSON.stringify(o.storageKey||"")}),\n inlineBoot: !!document.querySelector('#boot, .boot-loader'),\n loadingSession: [...document.querySelectorAll('span')].some((node) => (node.textContent || '').trim() === 'Loading session'),\n connecting: [...document.querySelectorAll('span')].some((node) => (node.textContent || '').trim().toLowerCase() === 'connecting…'),\n invitation: !!document.querySelector('#invitation-room-title'),\n roomStatus: document.querySelector('[data-tp-preview-room-status]')?.dataset.tpPreviewRoomStatus || null,\n heldLocks: locks.held?.length || 0,\n pendingLocks: locks.pending?.length || 0,\n };\n })()`,awaitPromise:!0,returnByValue:!0},l).catch(()=>({result:{value:{}}})),t=e.result?.value||{};throw new Error(`Authenticated preview did not leave its loading state within 15 seconds (session=${t.sessionPresent?"present":"missing"}, inlineBoot=${t.inlineBoot?"yes":"no"}, loadingSession=${t.loadingSession?"yes":"no"}, connecting=${t.connecting?"yes":"no"}, roomStatus=${t.roomStatus||"none"}, authLocks=${t.heldLocks||0}/${t.pendingLocks||0}, invitation=${t.invitation?"yes":"no"}, reads=${p.join(",")||"none"}, local=${w.join(",")||"none"}, runtime=${m.join(" | ")||"none"}); no screenshot was created.`)}}const R=o?Math.min(300,Math.max(0,Number(r)||0)):Math.min(d,Math.max(0,Number(r)||0));return R&&await new Promise(e=>setTimeout(e,R)),await i({pipe:s,sessionId:l})}finally{c?.(),u?.(),h?.(),l&&s.cancelWaiters(l);try{if(a)try{await s.send("Target.disposeBrowserContext",{browserContextId:a})}catch{await this.close()}}finally{try{await(n?.())}finally{this.jobs--,this.scheduleIdleClose()}}}}scheduleIdleClose(){clearTimeout(this.idleTimer),this.idleTimer=null,!this.jobs&&this.child&&(this.idleTimer=setTimeout(()=>{this.idleTimer=null,this.jobs||this.close().catch(()=>{})},this.idleTimeoutMs),this.idleTimer.unref?.())}async renderedState(e,t){const r=await e.send("Runtime.evaluate",{expression:"(() => {\n const visible = (node) => {\n if (!node) return false;\n const style = getComputedStyle(node);\n const rect = node.getBoundingClientRect();\n return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0 && rect.width > 0 && rect.height > 0;\n };\n const boot = document.querySelector('#boot');\n const meaningful = [...document.querySelectorAll('main,article,section,img,video,canvas,svg,[role=\"dialog\"],[role=\"main\"],[role=\"img\"]')]\n .filter((node) => !node.closest('#boot') && visible(node));\n return {\n title: document.title || '', url: location.href,\n hasBody: !!document.body,\n textLength: (document.body?.innerText || '').trim().length,\n meaningfulVisualCount: meaningful.length,\n visibleBoot: visible(boot),\n visibleLoading: [...document.querySelectorAll('span')].some((node) => {\n const text = (node.textContent || '').trim();\n return visible(node) && /^(loading session|restoring terminals…|connecting(?:…|...))$/i.test(text);\n }),\n signedOutInvite: !!document.querySelector('#invitation-room-title'),\n prerenderGeo: !!document.querySelector('[data-prerender=\"geo\"]'),\n };\n })()",returnByValue:!0},t);if(r.exceptionDetails)throw new Error("Could not inspect the rendered preview state.");return r.result?.value||null}async freezeCurrentPage(e,t){const r=await e.send("Runtime.evaluate",{expression:"(async () => {\n const clone = document.documentElement.cloneNode(true);\n clone.querySelectorAll('script,link[rel=\"modulepreload\"],link[rel=\"preload\"]').forEach((node) => node.remove());\n clone.querySelectorAll('*').forEach((node) => {\n for (const attr of [...node.attributes]) if (/^on/i.test(attr.name)) node.removeAttribute(attr.name);\n if (node instanceof HTMLInputElement) node.removeAttribute('value');\n if (node instanceof HTMLTextAreaElement) node.textContent = '';\n });\n const css = [];\n for (const sheet of [...document.styleSheets]) {\n try { css.push([...sheet.cssRules].map((rule) => rule.cssText).join('\\n')); } catch {}\n }\n clone.querySelectorAll('style,link[rel=\"stylesheet\"]').forEach((node) => node.remove());\n const head = clone.querySelector('head') || clone.insertBefore(document.createElement('head'), clone.firstChild);\n const style = document.createElement('style');\n style.setAttribute('data-thinkpool-snapshot', '');\n style.textContent = css.join('\\n');\n head.appendChild(style);\n\n const assetUrls = new Set();\n const remember = (raw) => {\n if (!raw || /^(data:|blob:|#)/i.test(raw)) return;\n try {\n const absolute = new URL(raw, location.href);\n if (absolute.origin === location.origin) assetUrls.add(absolute.href);\n } catch {}\n };\n clone.querySelectorAll('[src],[poster]').forEach((node) => {\n remember(node.getAttribute('src'));\n remember(node.getAttribute('poster'));\n });\n for (const match of style.textContent.matchAll(/url\\(\\s*(['\"]?)([^'\"\\)]+)\\1\\s*\\)/gi)) remember(match[2]);\n\n const replacements = new Map();\n let inlinedBytes = 0;\n for (const absolute of assetUrls) {\n try {\n const response = await fetch(absolute);\n const buffer = await response.arrayBuffer();\n if (!response.ok || buffer.byteLength > 512000 || inlinedBytes + buffer.byteLength > 1000000) continue;\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));\n replacements.set(absolute, 'data:' + (response.headers.get('content-type') || 'application/octet-stream') + ';base64,' + btoa(binary));\n inlinedBytes += buffer.byteLength;\n } catch {}\n }\n const replaceAsset = (raw) => {\n try { return replacements.get(new URL(raw, location.href).href) || raw; } catch { return raw; }\n };\n clone.querySelectorAll('[src],[poster]').forEach((node) => {\n for (const name of ['src', 'poster']) if (node.hasAttribute(name)) node.setAttribute(name, replaceAsset(node.getAttribute(name)));\n node.removeAttribute('srcset');\n });\n style.textContent = style.textContent.replace(/url\\(\\s*(['\"]?)([^'\"\\)]+)\\1\\s*\\)/gi, (_all, quote, raw) => 'url(\"' + replaceAsset(raw) + '\")');\n return '<!doctype html>\\n' + clone.outerHTML;\n })()",awaitPromise:!0,returnByValue:!0},t);if(r.exceptionDetails||"string"!=typeof r.result?.value){const e=r.exceptionDetails?.exception?.description||r.exceptionDetails?.text||r.result?.description;throw new Error("Could not freeze the rendered preview for Design"+(e?`: ${e}`:"."))}const o=r.result.value;if(!o.trim()||Buffer.byteLength(o)>19e5)throw new Error("The rendered preview is too large to make editable.");return o}async capture({url:e,viewport:t,fullPage:r=!0,waitMs:o=300,includeSnapshot:i=!1,auth:n=null}){return this.withPage({url:e,viewport:t,waitMs:o,auth:n},async({pipe:e,sessionId:o})=>{const n=await this.renderedState(e,o),s=await e.send("Page.getLayoutMetrics",{},o),a=Math.ceil(s.cssContentSize?.height||t.height),l=r?Math.min(2e4,Math.max(t.height,a)):t.height,c=await e.send("Page.captureScreenshot",{format:"png",fromSurface:!0,captureBeyondViewport:!0,clip:{x:0,y:0,width:t.width,height:l,scale:1}},o),u=await this.renderedState(e,o),d=i?await this.freezeCurrentPage(e,o):null;return{png:Buffer.from(c.data,"base64"),width:t.width,height:l,contentHeight:a,capped:a>2e4,beforeState:n,afterState:u,snapshot:d}})}async snapshot({url:e,viewport:t=DEFAULT_VIEWPORTS.desktop,waitMs:r=300,auth:o=null}){return this.withPage({url:e,viewport:t,waitMs:r,auth:o},({pipe:e,sessionId:t})=>this.freezeCurrentPage(e,t))}async inspect({url:e,viewport:t=DEFAULT_VIEWPORTS.mobile,selector:r,waitMs:o=300,auth:i=null}){return this.withPage({url:e,viewport:t,waitMs:o,auth:i},async({pipe:e,sessionId:t})=>{const o=`(() => {\n const selector = ${JSON.stringify(r||null)};\n const el = selector ? document.querySelector(selector) : null;\n const r = el && el.getBoundingClientRect();\n return {\n title: document.title, url: location.href,\n viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio, touchPoints: navigator.maxTouchPoints, coarsePointer: matchMedia('(pointer: coarse)').matches, userAgent: navigator.userAgent },\n document: { width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth || 0), height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0) },\n selector: selector ? { query: selector, found: !!el, tag: el?.tagName || null, text: el?.innerText?.slice(0, 2000) || '', rect: r ? { x:r.x, y:r.y, width:r.width, height:r.height } : null } : null,\n text: document.body?.innerText?.slice(0, 12000) || ''\n };\n })()`,i=await e.send("Runtime.evaluate",{expression:o,returnByValue:!0},t);if(i.exceptionDetails)throw new Error("DOM inspection failed in the preview page.");return i.result?.value||{}})}async close(){if(clearTimeout(this.idleTimer),this.idleTimer=null,this.closing)return this.closing;const e=this.child,t=this.pipe,o=this.profileDir;return this.child=null,this.pipe=null,this.profileDir=null,this.closing=(async()=>{if(t&&null==e?.exitCode&&null==e?.signalCode&&await t.send("Browser.close").catch(()=>{}),e&&null==e.exitCode)try{e.kill("SIGTERM")}catch{}o&&await r.rm(o,{recursive:!0,force:!0}).catch(()=>{})})().finally(()=>{this.closing=null}),this.closing}}export const sharedViewportBrowser=new CdpBrowser;export class ViewportManager{constructor({workspaceRoot:e,ownerId:t,roomCode:r,outbox:s,browser:a=sharedViewportBrowser,startPreviewImpl:c=l,designContext:u=null,authContext:d=null}={}){this.workspaceRoot=i.resolve(e||process.cwd()),this.ownerId=t||n(),this.roomCode="string"==typeof r&&r.trim()?r.trim():null,this.outbox=s||i.join(o.tmpdir(),"thinkpool-viewport-captures",this.ownerId),this.browser=a,this.startPreviewImpl=c,this.designContext="function"==typeof u?u:()=>null,this.authContext="function"==typeof d?d:()=>null,this.previewId=`viewport:${this.ownerId}`,this.preview=null,this.root=null}setWorkspaceRoot(e){if(!e)throw new Error("A lane workspace is required.");return this.workspaceRoot=i.resolve(e),this.workspaceRoot}async start({root:e="dist"}={}){const t=await resolveContainedRoot(this.workspaceRoot,e);await this.stop();const r=await this.startPreviewImpl({dir:t,host:"127.0.0.1",port:0,id:this.previewId}),o=new URL(r.url);if("127.0.0.1"!==o.hostname&&"localhost"!==o.hostname)throw await(r.stop?.()),new Error("Preview server did not bind to loopback.");return this.preview=r,this.root=t,{url:r.url,root:t}}requirePreview(){if(!this.preview?.url)throw new Error("No preview is running. Call preview_start after building the app.");return this.preview}defaultRoute(){return this.roomCode?`/?r=${encodeURIComponent(this.roomCode)}`:"/"}resolveRoute(e){return normalizeRoute(e??this.defaultRoute())}pageUrl(e){const t=this.requirePreview();return new URL(normalizeRoute(e),t.url).href}async previewAuth(e){if(!routeNeedsPreviewAuth(e))return null;let t=this.authContext()||{};if(!t.accessToken)return null;const r=Date.now()+15e3;for(;;){const e=previewAuthBootstrap({...t,minTtlSec:30});if(e)return e;if(Date.now()>=r)throw new Error("The bridge account token did not refresh in time for an authenticated preview; no screenshot was created.");await new Promise(e=>setTimeout(e,250)),t=this.authContext()||{}}}async capture({route:e,viewports:t="both",title:o="Viewport capture",fullPage:n=!0,waitMs:s=300,card:a=!1}={}){const l=this.resolveRoute(e),c=this.pageUrl(l),u="both"===t?["desktop","mobile"]:[t];if(u.some(e=>!DEFAULT_VIEWPORTS[e]))throw new Error('viewports must be "both", "desktop", or "mobile".');const d=`${safeSlug(o)}-${Date.now().toString(36)}`,h=await r.realpath(this.workspaceRoot),p=i.relative(h,this.root)||".",w=JSON.stringify([p,l]),m=this.designContext()||null,f=m?.captureKey===w?{designRequestId:m.requestId,parentRevision:m.parentRevision}:{},g=!0===a||!!f.designRequestId,b={};for(const e of u)b[e]=await this.browser.capture({url:c,viewport:DEFAULT_VIEWPORTS[e],fullPage:n,waitMs:s,includeSnapshot:g&&e===(u.includes("desktop")?"desktop":u[0]),auth:await this.previewAuth(l)});const v=g&&!(!b.desktop||!b.mobile);if(v||routeNeedsPreviewAuth(l))for(const[e,t]of Object.entries(b)){const r=previewArtifactIssue(l,t);if(r)throw new Error(`${e} preview rejected: ${r} No screenshot or mockup card was created.`)}const y=b.desktop?.snapshot||b.mobile?.snapshot||null;if(v&&("string"!=typeof y||!y.trim()))throw new Error("The deliverable could not freeze its rendered source. No mockup card was created.");await r.mkdir(this.outbox,{recursive:!0});for(const[e,t]of Object.entries(b))t.file=i.join(this.outbox,`${d}--${e}.png`),await r.writeFile(t.file,t.png);const x=v?i.join(this.outbox,`${d}--source.html`):null;x&&await r.writeFile(x,y);let S=null;if(v){const e=await resolvePreviewSourceMap(this.root),t={slug:d,title:String(o||"Viewport capture").slice(0,120),desktop:b.desktop.file,mobile:b.mobile.file,ts:Date.now(),sourceKind:"preview",deliveryIntent:"card",readinessValidated:!0,snapshot:x,previewRoot:p,route:l,captureKey:w,...e?{sourceMap:e}:{},...f};S=i.join(this.outbox,`${d}.json`);const n=`${S}.tmp`;await r.writeFile(n,JSON.stringify(t)),await r.rename(n,S)}return{url:c,route:l,slug:d,manifestPath:S,captures:b,snapshotPath:x,cardReady:v}}async inspect({route:e,viewport:t="mobile",selector:r,waitMs:o=300}={}){if(!DEFAULT_VIEWPORTS[t])throw new Error('viewport must be "desktop" or "mobile".');const i=this.resolveRoute(e);return this.browser.inspect({url:this.pageUrl(i),viewport:DEFAULT_VIEWPORTS[t],selector:r,waitMs:o,auth:await this.previewAuth(i)})}async stop(){const e=this.preview||a.get(this.previewId);this.preview=null,this.root=null,e?.stop&&await e.stop()}}const w=e=>({content:[{type:"text",text:e}]}),m=e=>w(`Viewport error: ${e?.message||e}`);export function createViewportTools({tool:e,z:t,manager:r}){return[e("preview_start","Start a bridge-owned, read-only localhost preview for built files inside this lane workspace. Run the project build first. The default root is dist; pass another relative directory only when its index.html is the intended preview.",{root:t.string().max(240).optional().describe("relative directory inside this lane workspace; default: dist")},async e=>{try{const t=await r.start({root:e?.root||"dist"});return w(`Preview ready from ${i.relative(r.workspaceRoot,t.root)||"."}. Use preview_capture to see desktop/mobile, or preview_inspect for DOM text and geometry.`)}catch(e){return m(e)}}),e("preview_capture","Capture this lane preview in a fresh isolated browser at exact desktop (1440×900) and mobile (390×844) CSS viewports. Omit path to preview the current authenticated room; pass / explicitly for the public landing page. Defaults to both and full-page. Captures are inline verification evidence by default. Room and protected routes automatically use the bridge account as a read-only authenticated visual harness; external REST writes, presence, and room broadcasts are blocked, apart from non-mutating roster/transcript snapshot requests. Set card=true only for a user-requested mockup or visual deliverable that should remain editable in the transcript; complete desktop+mobile settled content is then required and the card arrives after the final response.",{path:t.string().max(500).optional().describe("route within the preview; omit for the current authenticated room, or pass / for the public landing page; never a full URL"),viewports:t.enum(["both","desktop","mobile"]).optional(),title:t.string().max(120).optional(),fullPage:t.boolean().optional(),card:t.boolean().optional().describe("default false; true only for an intentional user-facing mockup/Design deliverable"),waitMs:t.number().int().min(0).max(d).optional().describe("settle time after load, max 5000ms")},async e=>{try{const t=await r.capture({route:e?.path,viewports:e?.viewports||"both",title:e?.title||"Viewport capture",fullPage:!1!==e?.fullPage,waitMs:e?.waitMs??300,card:!0===e?.card}),o=t.cardReady?"The complete room preview card is queued after your final response.":"This is verification evidence only; no room card was created.",i=[{type:"text",text:`Captured ${Object.keys(t.captures).join(" + ")} for ${t.route}. ${o}`}];for(const[e,r]of Object.entries(t.captures))i.push({type:"text",text:`${e}: ${r.width}×${r.height}${r.capped?" (height capped)":""}`}),i.push({type:"image",data:r.png.toString("base64"),mimeType:"image/png"});return{content:i}}catch(e){return m(e)}}),e("preview_inspect","Inspect rendered DOM state in this lane preview at an exact desktop or mobile viewport. Omit path for the current authenticated room; pass / explicitly for the public landing page. Returns page text, document dimensions, and optional selector text/geometry without changing the page.",{path:t.string().max(500).optional().describe("route within the preview; omit for the current authenticated room, or pass / for the public landing page; never a full URL"),viewport:t.enum(["desktop","mobile"]).optional(),selector:t.string().max(500).optional(),waitMs:t.number().int().min(0).max(d).optional()},async e=>{try{const t=await r.inspect({route:e?.path,viewport:e?.viewport||"mobile",selector:e?.selector,waitMs:e?.waitMs??300});return w(JSON.stringify(t,null,2))}catch(e){return m(e)}}),e("preview_stop","Stop this lane's bridge-owned preview server and release its port.",{},async()=>{try{return await r.stop(),w("Preview stopped.")}catch(e){return m(e)}})]}