thinkpool-pair 0.7.445 → 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.
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "format": 1,
3
3
  "package": "thinkpool-pair",
4
- "version": "0.7.445",
4
+ "version": "0.7.448",
5
5
  "kind": "minified",
6
- "minifiedModules": 147,
7
- "sourceModuleBytes": 1955402,
8
- "artifactModuleBytes": 892712,
6
+ "minifiedModules": 150,
7
+ "sourceModuleBytes": 1976056,
8
+ "artifactModuleBytes": 903265,
9
9
  "files": {
10
- "package.json": "11845b6631a5a83d450a36e95b5a2743d4bd85fd3c5f3a78abfcb667802844f7",
10
+ "package.json": "a1aa09ae41663a65320fdea0ec238fb6e543364ae97a2819ff1b25fa160160b0",
11
11
  "LICENSE": "8a030197ca1cad9669909ca14c9ad15cf48d9fe551da526ccaf1fca993eddb23",
12
- "bridge.mjs": "3165b21f88d877d1611a9363a2f3c5aa387873ae0ccba4797d841b1d03cff116",
12
+ "bridge.mjs": "1bab3b6045603450a92eb3199568e05a8842a18dcfb31fdbe93d413c7fb1ae7f",
13
13
  "bridge-service.sh": "b64f27c473ca8f3a49190fd08c23947ab14ca9958d70b6fa53c7bddc2be91403",
14
14
  "command-guidance.mjs": "b9b967b59ff4e77c9613abb0f28858bf3ac5b106a509e471039777b53bec3e29",
15
15
  "abort-turn-barrier.mjs": "3734a5b7e2e940ec6d7fa8b5b04935e1429a23d1bd9e92b2ac2a2ecd9f77c75e",
@@ -30,12 +30,12 @@
30
30
  "code-automations.mjs": "03c70058bc6d5a0b245f3fb8f5e2cb629c5ef7c1c6cd7d7fa018c40523b91036",
31
31
  "terminal-name.mjs": "f39bb8d1e148ab9a74cbb96438d31e524d6597b8035f00d7a906c28c306c0cf8",
32
32
  "claude-command-catalog.mjs": "36bd4342b2ae94b43a0072533ef2d1648c7473003964bbad433fdb1a8b2bfec1",
33
- "codex-session.mjs": "8e122c384df76dceee64cb4ff6b1407801b76c01cd6fa5875c7c31d6baa0e853",
33
+ "codex-session.mjs": "888e346a31e0a7fe22d813f227db1df46d49a63bfe178a17502a22f6292a5cb6",
34
34
  "codex-host-heal.mjs": "14ea191ed2072203be2edd40a41272f176aafc044753919f65b1071ce48f060f",
35
35
  "question-response.mjs": "35a7ba53d3eb117717c18004d2a8441f8f37665b231a3c44f4bd9321ff59dd57",
36
36
  "thinkpool-room-prompt.mjs": "cbae9bba173afad90eec21cfc726baec72cc26de4e5b96c25098755fc61f02c8",
37
37
  "thinkpool-prompt-contracts.mjs": "051602d7d2fe9b34df92957bd3922c5eeebedbcc60c892adf415243887d8c62f",
38
- "thinkpool-capabilities.json": "1be75fd52f94cd4de416b9c032e129bd6d974d3e14c0037da133428f99385e2b",
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",
@@ -81,7 +81,7 @@
81
81
  "git-diff-report.mjs": "a428b3129c90e207a125a86c4485ec16c8ee7e81d432060a070e37172d85ce7a",
82
82
  "runtime-session.mjs": "ff97a7a65aba31d176d1afc8d0dc9d11a217415c8b0b7660a5b33c17756576c4",
83
83
  "turn-stall.mjs": "88c712d45974e492bfcfdbd93baa371bb8be1ddb754dacb18fae20da4dd672db",
84
- "update-gate.mjs": "55e2900307677f9402b9cf0fde5371abe6458f5bb590e1f3c82555c4be42ddf2",
84
+ "update-gate.mjs": "78add5d64b8a880ad061751a7a9aaebf4955484fa751c9e3dc4403658a5e3f7b",
85
85
  "agent-notify.mjs": "8e87f448c28205382000d4ddd8e119932a80dc0e0d5b840c780718b0166461cc",
86
86
  "agent-detect.mjs": "f38ede090e9caa8f7cde0febc41d9dc2c4301e1cd6017a07517b96364ebafb8c",
87
87
  "code-event-contract.mjs": "75c0e0c8b6acb19cab4168a543c847ab74232d62c35f9f9a791d4268aefa72d7",
@@ -98,7 +98,9 @@
98
98
  "event-delivery-queue.mjs": "0ed00938cca30627896a944fbdf8da21f91341e6d96a3ca190777843b302025f",
99
99
  "transport-frame.mjs": "729d5a5de85a9f4bf026b8759eb2a6ca2ad408194e0c6176428f759197278cfd",
100
100
  "turn-admission.mjs": "d327a8a1226dfde72c71a2585f9e69230cb8cdfc978e78556241c3403156d06e",
101
- "durable-command-journal.mjs": "787142debc5472299155b32d0684d594e9c577b669e50554b9b3b38619355e2c",
101
+ "durable-command-journal.mjs": "34410bc18bf2c7f6c4ea61aca2f19101e5ea039bcb5fd5fc9133d444eb6cb14e",
102
+ "host-control-channels.mjs": "6f8d3b75997ea4af45892a540d59a82c2e6c47003c4c6600c2c2cb770ac66ca4",
103
+ "recovery-diagnostics.mjs": "376e9b140bee65c740cc53d25947cb1758269043d7c584e201cd0d808f5da47c",
102
104
  "mcp-flight-recorder.mjs": "30bf77f4d567f0449856d6da09f7a0a9c8d7e12c5dcaf200dc9e511a331425be",
103
105
  "plan-meters.mjs": "f7f5aeb3d6a3462b754d3724367bbb331643ba58108e29e91b2aca2f613ab499",
104
106
  "recap.mjs": "10e67aa933fa8bac4059878167d578f87db3184d7affd168c46c5134c6e97c8b",
@@ -108,11 +110,11 @@
108
110
  "session-store.mjs": "dc3dac5e9c2f7664f78647d77cb974e211e2c68a3f97d45c605b419927a81a72",
109
111
  "side-lane.mjs": "6a6bc6d8041872ba4472d70fd1cece62607417982a72b8721f4fbed77655cc6d",
110
112
  "worker-completion.mjs": "3eab6abf9cde32e62b8bef676023bd59759836f88887d578d2db2e9fa092a151",
111
- "cross-terminal.mjs": "08d3889516f0b7edd7628dccb61b32731ab1e033efcc19f597a3d29a053fe020",
113
+ "cross-terminal.mjs": "239ee58df080219022579e782ea6709397efb794d13187b6175fbd4929f1ae00",
112
114
  "cloud-session-transcript.mjs": "1e8b3000d628eac0cff171901d6fed13983528b42e23e277583ef65bf8f9fa2b",
113
115
  "pair-bus.mjs": "cbc28cee7f4f06b2213091e0b7b07d61c1d01f0ff207c5c6d808cd616ece221e",
114
116
  "direct-pair-room.mjs": "92557b54307da2ca31f0a0d090e44b019a23b1ab32a7dfd6cbcb9d55fd761736",
115
- "lane-lifecycle.mjs": "9b5ae3d105c30e3ad37f1f1f792200da2dd4d2bb5c4979a2a69618373e58f1c8",
117
+ "lane-lifecycle.mjs": "484e2bb00ae1fee388b94dc4103af1b6c07989a763d6ee0bd20dce9fb51c1951",
116
118
  "lane-continuation.mjs": "b12ee9bb2b8625635c9fc20327acefd4c3ed41e716fdf212d0f7f53f514f8e93",
117
119
  "interrupted-resume.mjs": "df46a5fa07fad734dc3a56643aa44e24121e4afdd8206cc5e123102dad87f901",
118
120
  "dispatch-lease.mjs": "6e951cad5fb02999ac27e1cf03d9f932b079b2184853b8c6f9b5a8d579fcbbdd",
@@ -122,10 +124,11 @@
122
124
  "flow-task-graph.mjs": "52f555219c6c940c5d33c6c429d132ab2ded46f9ac6d5207bd5ad972067493bf",
123
125
  "flow-models.mjs": "ef34d9a2b091280fd1cc289e77843bb4606f43534cca8001b823abe492fda45a",
124
126
  "flow-host-revert.mjs": "a6da239fc54723c74ed51fc03ae3203458b92d2a2e33c26576a9e62cd1031fea",
125
- "flow-preview.mjs": "b4f1fb6aeed49623c0264ef47d02ec0b244c055ba4386f5ba4c1ffdcd02824c5",
127
+ "flow-preview.mjs": "d38cb7f292d41600cde85eef335b0793f5bad7583f9f851f886ef48c4a91b4f1",
126
128
  "mockup-delivery.mjs": "fa112408a186d856b343157381188dc18502aba6cf7ef365459f6c5a074e9cd6",
127
- "viewport.mjs": "80cb9b6e9275499397239a8b4288c0b49791886696a46917ac798acb75267ee5",
128
- "design-edit.mjs": "8e34351dcad2bea3e3ee1b0e57bdd28aeb047ac2e2b3f30c0d86dc1fd3575561",
129
+ "viewport.mjs": "cdbf8f6617e8b840c1317b91580eff8d2c7f86d3d476651bcb1bdd657f23c101",
130
+ "browser-job.mjs": "391f92c293ba6da419089eb7eaaf942ba44339d1bdba87bb56e23e51d1e94fd0",
131
+ "design-edit.mjs": "23df0a89b8f6f4df19b7eac55e3bee0ef277c9cddd632d9c3bd0197b2a35059e",
129
132
  "realtime-auth.mjs": "f18260716dbca71f420cafac7f09ce435df56de217174ee50196e9be9cf93ecc",
130
133
  "design-source-contract.mjs": "18bcd024b317293e445c433fc9c59f9eb85e8fb3fa4de824b82604bbae4687b8",
131
134
  "flow-review.mjs": "9d64fb9e95cd685559df20f1fc778de10c9e02fb0e662ae37699e26c632d0a40",
@@ -159,7 +162,7 @@
159
162
  "providers.mjs": "c0757433d30bf2f22e80f85e779cdeeeb10b412d568965f1115d4de2c8aa0314",
160
163
  "provider-resilience.mjs": "a5a3921040d1860bb82f1d3b53c6ec25a5e230345c0131f2213c535e07fc497e",
161
164
  "model-prices.mjs": "91f0bd8688f59ce6bff7743710d3b1139f4f0823d6b95fa68cd9b6fdcdba6ab4",
162
- "README.md": "3e243a1b9c3ad9e7a2b824ccac56e447e6de7f2616b8d3d1c19cc63438f9915b",
165
+ "README.md": "9f423baed047976763c8fbcab81366ba0ce5872844101d8b123d6cdfb375895b",
163
166
  "publish-guard.mjs": "569dd788790e68ea822129f588fd16c1bb435324666552fe9dfe7c435ab3f617"
164
167
  }
165
168
  }
@@ -0,0 +1 @@
1
+ const e=new Set(["bootstrap","build","joined","error","rejoin","visibility","online","offline","phase"]),t=new Set(["visibility","pageshow","online","foreground-retry","terminal-retry"]),n=new Set(["SUBSCRIBED","CHANNEL_ERROR","TIMED_OUT","CLOSED","idle","offline","reconnecting","syncing","caught-up","connected","connecting","disconnected","disconnecting","open","closed","visible","hidden"]),i=new Set(["auth-expired","authorization","timeout","transport","unknown"]),r=new Set(["room-mounted","metadata-ready","transcript-seed-ready","roster-ready","replay-ready","room-presence-ready","bridge-present","account-presence-ready","initial-view-painted"]),o=e=>Number.isFinite(e)?Math.min(864e5,Math.max(0,Math.round(e))):void 0;export function connectionFailure(e){const t=String(e?.message||e||"");return/jwt|token.*expir|expir.*token/i.test(t)?"auth-expired":/unauthori[sz]ed|permission|forbidden/i.test(t)?"authorization":/timeout|timed.out/i.test(t)?"timeout":/socket|network|connection/i.test(t)?"transport":"unknown"}function s(s){if(!s||!e.has(s.event))return null;const a={event:s.event};for(const e of["ms","attempt","ageMs"]){const t=o(s[e]);void 0!==t&&(a[e]=t)}return t.has(s.reason)&&(a.reason=s.reason),n.has(s.state)&&(a.state=s.state),n.has(s.socket)&&(a.socket=s.socket),i.has(s.failure)&&(a.failure=s.failure),r.has(s.phase)&&(a.phase=s.phase),"boolean"==typeof s.online&&(a.online=s.online),"boolean"==typeof s.hidden&&(a.hidden=s.hidden),a}export function createConnectionDiagnostics({storage:e,key:t,now:n=Date.now,randomId:i=()=>globalThis.crypto?.randomUUID?.()||`c${Math.random().toString(36).slice(2)}`,platform:r="other"}={}){let a=i(),c=[];try{const i=JSON.parse(e?.getItem(t)||"null");/^[\w-]{1,64}$/.test(i?.clientId)&&(a=i.clientId),c=(Array.isArray(i?.entries)?i.entries:[]).slice(-32).flatMap(e=>{const t=s(e);return t&&Number.isFinite(e.at)&&n()-e.at<864e5?[{...t,at:e.at}]:[]}).slice(-32)}catch{}return{record(i,r={}){const o=s({...r,event:i});o&&(c.push({...o,at:n()}),c=c.slice(-32),(()=>{try{e?.setItem(t,JSON.stringify({clientId:a,entries:c}))}catch{}})())},snapshot:()=>({version:1,clientId:a,platform:["ios","android"].includes(r)?r:"other",entries:c.map(({at:e,...t})=>({...t,ageMs:o(n()-e)}))})}}export function createRecoveryDiagnosticsReceiver({now:e=Date.now,emit:t}={}){let n=0,i=0;const r=new Map;return o=>{if(1!==o?.version||!/^[\w-]{1,64}$/.test(o?.clientId)||!Array.isArray(o.entries))return!1;if(o.entries.length<1||o.entries.length>32)return!1;const a=o.entries.map(s);if(a.some(e=>!e))return!1;const c=e();return c-n>=6e4&&(n=c,i=0,r.clear()),!(i>=12||r.has(o.clientId)&&c-r.get(o.clientId)<1e4||(r.set(o.clientId,c),i++,t({version:1,clientId:o.clientId,platform:["ios","android"].includes(o.platform)?o.platform:"other",entries:a}),0))}}
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 43,
3
+ "bundleVersion": 45,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
7
- "version": 12,
7
+ "version": 13,
8
8
  "routes": [
9
9
  {
10
10
  "id": "room-awareness",
11
11
  "tools": ["read_terminal"],
12
12
  "trigger": "\\b(other|another|sibling|peer)\\s+(lane|terminal)|\\bread_terminal\\b",
13
- "prompt": "When the request depends on another lane, a sibling may overlap the work, or a person refers to another terminal, use the current ROOM NOW snapshot as the default roster. Do not repeat it with a no-argument read_terminal call unless the snapshot is missing or truncated; that roster lookup has its own one-call allowance. Terminal names are static room-record identities. When a person supplies a terminal name, including an ordinal-looking name such as T112, resolve that exact stored name through ROOM NOW or a targeted read_terminal call before interpreting it as a positional label. Never infer a terminal from roster order, task topic, product name, or an apparent UI position. Only an unmatched generic placeholder such as Terminal 2 is positional and unsafe. Read only the relevant terminal by ref or name when its detailed transcript is actually needed. Never poll with read_terminal: wait for the room snapshot or completion signal to show a state change, then collect each finished owned worker once. Leave every finished worker visible for review until a person dismisses it with the Ensemble X. Never call close_terminal merely because work finished. Preserve the bounded targeted-read allowance for worker collection and final review."
13
+ "prompt": "When the request depends on another lane, a sibling may overlap the work, or a person refers to another terminal, use the current ROOM NOW snapshot as the default roster. Do not repeat it with a no-argument read_terminal call unless the snapshot is missing or truncated; that roster lookup has its own one-call allowance. read_terminal supports explicit self lookup. Terminal names are static room-record identities. When a person supplies a terminal name, including an ordinal-looking name such as T112, resolve that exact stored name through ROOM NOW or a targeted read_terminal call before interpreting it as a positional label. Never infer a terminal from roster order, task topic, product name, or an apparent UI position. Only an unmatched generic placeholder such as Terminal 2 is positional and unsafe. Read only the relevant terminal by ref or name when its detailed transcript is actually needed. Never poll with read_terminal: wait for the room snapshot or completion signal to show a state change, then collect each finished owned worker once. Leave every finished worker visible for review until a person dismisses it with the Ensemble X. Never call close_terminal merely because work finished. Preserve the bounded targeted-read allowance for worker collection and final review."
14
14
  },
15
15
  {
16
16
  "id": "cross-room-awareness",
@@ -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/update-gate.mjs CHANGED
@@ -1 +1 @@
1
- export function isSafeToRestart({pendingUpdate:e,stopping:t=!1,childIdle:n,childBetween:r,childPeer:i,requested:o=!1}){if(!e||t)return!1;if(o){const e=r instanceof Map?r:n;for(const t of e.values())if(!t)return!1;return!0}for(const e of n.values())if(!e)return!1;return!0}export function turnInFlight(e){for(const t of e?.values?.()||[])if(t&&("boolean"==typeof t.session?.turnActive?t.session.turnActive:t.turnActive)||Number(t?.pending?.size||0)>0)return!0;return!1}export function realtimeRecoveryDecision({brokenSince:e,now:t,httpHealthy:n,activeTurn:r,graceMs:i=6e4,activeTurnMaxMs:o=3e5}){const s=t-e;return!e||s<i?"wait":n?r&&s<o?"hold-active":"restart":"hold-offline"}
1
+ export function isSafeToRestart({pendingUpdate:e,stopping:n=!1,childIdle:t,childBetween:r,childPeer:i,requested:o=!1}){if(!e||n)return!1;if(o){const e=r instanceof Map?r:t;for(const n of e.values())if(!n)return!1;return!0}for(const e of t.values())if(!e)return!1;return!0}export function turnInFlight(e){for(const n of e?.values?.()||[]){const e=n&&("boolean"==typeof n.session?.turnActive?n.session.turnActive:n.turnActive),t=!1===n?._busyAnn&&Number(n?._turnRev)>0&&Number(n?._settledTurnRev)===Number(n?._turnRev);if(!0===n?._initialDispatchPending||!0===n?._busyAnn||e&&!t||Number(n?.pending?.size||0)>0)return!0}return!1}export function realtimeRecoveryDecision({brokenSince:e,now:n,httpHealthy:t,activeTurn:r,graceMs:i=6e4,activeTurnMaxMs:o=3e5}){const u=n-e;return!e||u<i?"wait":t?r&&u<o?"hold-active":"restart":"hold-offline"}
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)}})]}