@superblocksteam/library 2.0.167-next.0 → 2.0.169-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { n as consoleLogAttributes, t as LibraryEarlyConsoleBuffer } from "../early-console-buffer-DMR1SCsN.js";
2
- import { C as iframeMessageHandler, E as isEditMode, S as getParentOrigin, T as sendMessageImmediately, _ as generateId, a as tracker, b as editorBridge, c as resolveById, d as useSuperblocksContext, f as useSuperblocksDataTags, g as embedStore, h as useSuperblocksUser, i as getContextFromTraceHeaders, l as SuperblocksContextProvider, m as useSuperblocksProfiles, n as root_store_default, o as addNewPromise, p as useSuperblocksGroups, r as createIframeSpan, s as rejectById, t as ORCHESTRATOR_EXECUTE_RESPONSE_SIZE_LIMIT_FLAG, u as getAppMode, v as sendNotification, w as isEmbeddedBySuperblocksFirstParty, x as getEditStore, y as colors } from "../root-store-BYYXPQRA.js";
2
+ import { C as iframeMessageHandler, E as isEditMode, S as getParentOrigin, T as sendMessageImmediately, _ as generateId, a as tracker, b as editorBridge, c as resolveById, d as useSuperblocksContext, f as useSuperblocksDataTags, g as embedStore, h as useSuperblocksUser, i as getContextFromTraceHeaders, l as SuperblocksContextProvider, m as useSuperblocksProfiles, n as root_store_default, o as addNewPromise, p as useSuperblocksGroups, r as createIframeSpan, s as rejectById, t as ORCHESTRATOR_EXECUTE_RESPONSE_SIZE_LIMIT_FLAG, u as getAppMode, v as sendNotification, w as isEmbeddedBySuperblocksFirstParty, x as getEditStore, y as colors } from "../root-store-B4NzJHt8.js";
3
3
  import { n as initTracerProviderWithOrigin } from "../utils-DXaAwgS_.js";
4
- import { r as updateSdkApiSourceCode, t as discoverAndRegisterSdkApis } from "../sdk-api-discovery-DvvsdT1z.js";
5
- import { a as FixWithClarkButton, c as ErrorContent, d as ErrorMessage$1, f as ErrorStack, g as StyledClarkIcon, h as SecondaryButton, i as getWidgetRectAnchorName, l as ErrorDetails, m as ErrorTitle, n as useJSXContext, o as ActionsContainer, p as ErrorSummary, r as getWidgetAnchorName, s as ErrorContainer, u as ErrorIconContainer } from "../jsx-wrapper-1kyr6-x3.js";
4
+ import { r as updateSdkApiSourceCode, t as discoverAndRegisterSdkApis } from "../sdk-api-discovery-D6Xp39R4.js";
5
+ import { a as FixWithClarkButton, c as ErrorContent, d as ErrorMessage$1, f as ErrorStack, g as StyledClarkIcon, h as SecondaryButton, i as getWidgetRectAnchorName, l as ErrorDetails, m as ErrorTitle, n as useJSXContext, o as ActionsContainer, p as ErrorSummary, r as getWidgetAnchorName, s as ErrorContainer, u as ErrorIconContainer } from "../jsx-wrapper-CcDmmMUB.js";
6
6
  import { PropsCategory } from "@superblocksteam/library-shared/props";
7
7
  import { action, computed, makeAutoObservable, makeObservable, observable, reaction, when } from "mobx";
8
8
  import { UNSAFE_DataRouterContext, generatePath, useLocation, useNavigate, useParams, useRouteError } from "react-router";
@@ -2389,6 +2389,7 @@ var OperationAPI = class {
2389
2389
  asyncSocket = new AsyncSocket();
2390
2390
  isocket = null;
2391
2391
  applicationId;
2392
+ socketPath = "/sb-updates";
2392
2393
  connectionGeneration = 0;
2393
2394
  currentAuthorization;
2394
2395
  currentAuthorizationAllowsLegacy = false;
@@ -2399,12 +2400,14 @@ var OperationAPI = class {
2399
2400
  * @throws {Error} if the websocket connection can't be initiated within the
2400
2401
  * retry budget (RETRY_BUDGET_MS of backoff retries)
2401
2402
  */
2402
- async connect({ peerId, userId, authorization, applicationId, traceContext, connectionType, connectionTarget, connectionStartTime, isRetry }) {
2403
+ async connect({ peerId, userId, authorization, applicationId, traceContext, connectionType, connectionTarget, connectionStartTime, isRetry, socketPath = "/sb-updates" }) {
2404
+ if (socketPath !== "/sb-updates" && socketPath !== "/sb-dev-updates") throw new Error("Unsupported dev server socket path");
2403
2405
  if (!isRetry) {
2404
2406
  this.retryAttempts = 0;
2405
2407
  this.retryWindowStartMs = Date.now();
2406
2408
  }
2407
2409
  this.applicationId = applicationId;
2410
+ this.socketPath = socketPath;
2408
2411
  if (this.currentAuthorization && !this.currentAuthorizationAllowsLegacy && !isDevServerAuthorization(this.currentAuthorization, applicationId)) this.currentAuthorization = void 0;
2409
2412
  if (shouldUseAuthorizationCandidate(this.currentAuthorization, authorization, applicationId)) {
2410
2413
  this.currentAuthorization = authorization;
@@ -2419,6 +2422,7 @@ var OperationAPI = class {
2419
2422
  }
2420
2423
  try {
2421
2424
  const result = await connectSocket(this.serverUrl, {
2425
+ socketPath,
2422
2426
  peerId,
2423
2427
  userId,
2424
2428
  getAuthorization: () => this.currentAuthorization,
@@ -2643,6 +2647,7 @@ var OperationAPI = class {
2643
2647
  return;
2644
2648
  }
2645
2649
  await this.connect({
2650
+ socketPath: this.socketPath,
2646
2651
  peerId,
2647
2652
  userId,
2648
2653
  authorization: this.currentAuthorization,
@@ -2679,9 +2684,9 @@ function startSocketHeartbeat(socket, isCurrentConnection, onAuthorizationFailur
2679
2684
  clearTimeout(timeoutId);
2680
2685
  };
2681
2686
  }
2682
- async function connectSocket(serverUrl, { peerId, userId, applicationId, getAuthorization, onClose, isCurrentConnection, onAuthorizationFailure, onAuthorizationSuccess, traceContext, connectionType, connectionTarget, connectionStartTime }) {
2687
+ async function connectSocket(serverUrl, { socketPath, peerId, userId, applicationId, getAuthorization, onClose, isCurrentConnection, onAuthorizationFailure, onAuthorizationSuccess, traceContext, connectionType, connectionTarget, connectionStartTime }) {
2683
2688
  if (!serverUrl) return;
2684
- const wsUrl = new URL("/sb-updates", serverUrl);
2689
+ const wsUrl = new URL(socketPath, serverUrl);
2685
2690
  wsUrl.searchParams.set("peerId", peerId);
2686
2691
  if (userId) wsUrl.searchParams.set("userId", userId);
2687
2692
  wsUrl.searchParams.set("applicationId", applicationId);
@@ -4004,9 +4009,133 @@ function installEditModeErrorCapture(deps = {}) {
4004
4009
  };
4005
4010
  }
4006
4011
  //#endregion
4012
+ //#region src/edit-mode/uncaptured-images.ts
4013
+ /** modern-screenshot's own default placeholder: a 1x1 transparent GIF. */
4014
+ const TRANSPARENT_PLACEHOLDER = "data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
4015
+ /** Sizes in captured pixels, so stripes and label look the same at any scale. */
4016
+ const STRIPE_PERIOD_PX = 12;
4017
+ const LABEL_FONT_PX = 11;
4018
+ /** Rendered width of the label in em at this weight, with some margin. */
4019
+ const LABEL_WIDTH_EM = 10.5;
4020
+ function labelFits({ width, height }, scale) {
4021
+ return width * scale >= LABEL_FONT_PX * LABEL_WIDTH_EM && height * scale >= LABEL_FONT_PX * 2;
4022
+ }
4023
+ const round = (value) => Math.round(value * 100) / 100;
4024
+ /**
4025
+ * Drawn at the box's own CSS size so the stripes stay coarse enough to see on
4026
+ * a small image; a larger `<img>` sharing the URL scales it up to cover.
4027
+ */
4028
+ function uncapturedPlaceholder(box, options) {
4029
+ const width = round(box.width);
4030
+ const height = round(box.height);
4031
+ const period = round(STRIPE_PERIOD_PX / options.scale);
4032
+ const fontSize = round(LABEL_FONT_PX / options.scale);
4033
+ const label = options.label ? `<text x="50%" y="50%" font-family="sans-serif" font-size="${fontSize}" font-weight="600" fill="#52525b" text-anchor="middle" dominant-baseline="middle">Image not captured</text>` : "";
4034
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid slice"><defs><pattern id="h" width="${period}" height="${period}" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="${period}" height="${period}" fill="#f4f4f5"/><rect width="${round(period / 2)}" height="${period}" fill="#d4d4d8"/></pattern></defs><rect width="100%" height="100%" fill="url(#h)"/>` + label + `</svg>`;
4035
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
4036
+ }
4037
+ /**
4038
+ * Covers everything modern-screenshot clones: the root itself, open shadow
4039
+ * roots, and same-origin iframes.
4040
+ */
4041
+ function collectImages(root, images = []) {
4042
+ for (const el of [root, ...Array.from(root.querySelectorAll("*"))]) {
4043
+ if (el.tagName === "IMG") images.push(el);
4044
+ if (el.shadowRoot) for (const child of Array.from(el.shadowRoot.children)) collectImages(child, images);
4045
+ if (el.tagName === "IFRAME") {
4046
+ let body;
4047
+ try {
4048
+ body = el.contentDocument?.body;
4049
+ } catch {}
4050
+ if (body) collectImages(body, images);
4051
+ }
4052
+ }
4053
+ return images;
4054
+ }
4055
+ /** Strict overlap: an image that only touches the crop's edge is not in it. */
4056
+ function overlaps(box, crop) {
4057
+ return box.left < crop.x + crop.width && box.left + box.width > crop.x && box.top < crop.y + crop.height && box.top + box.height > crop.y;
4058
+ }
4059
+ /**
4060
+ * modern-screenshot fetches each URL once, so every `<img>` sharing a URL gets
4061
+ * the same placeholder; the smallest box decides whether it fits a label.
4062
+ *
4063
+ * With a `crop`, the page is rendered whole and then cropped, so an image
4064
+ * can be hatched yet absent from the result. Each URL records whether any of
4065
+ * its boxes lands inside the crop; positions are taken relative to the root,
4066
+ * the space the crop is in. An image inside a same-origin iframe is laid out
4067
+ * in the iframe's own space and is treated as inside rather than hidden.
4068
+ */
4069
+ function collectRenderedBoxes(root, crop) {
4070
+ const boxes = /* @__PURE__ */ new Map();
4071
+ const rootRect = crop ? root.getBoundingClientRect() : void 0;
4072
+ for (const img of collectImages(root)) {
4073
+ if (!img.complete || img.naturalWidth === 0) continue;
4074
+ const rect = img.getBoundingClientRect();
4075
+ const { width, height } = rect;
4076
+ if (width === 0 || height === 0) continue;
4077
+ const inCrop = !crop || !rootRect || img.ownerDocument !== root.ownerDocument || overlaps({
4078
+ left: rect.left - rootRect.left,
4079
+ top: rect.top - rootRect.top,
4080
+ width,
4081
+ height
4082
+ }, crop);
4083
+ for (const src of new Set([img.currentSrc, img.src])) {
4084
+ if (!src) continue;
4085
+ const known = boxes.get(src);
4086
+ const smaller = !known || width * height < known.size.width * known.size.height;
4087
+ boxes.set(src, {
4088
+ size: smaller ? {
4089
+ width,
4090
+ height
4091
+ } : known.size,
4092
+ inCrop: inCrop || (known?.inCrop ?? false)
4093
+ });
4094
+ }
4095
+ }
4096
+ return boxes;
4097
+ }
4098
+ /**
4099
+ * Only `<img>` elements the page displayed are counted and hatched. CSS
4100
+ * backgrounds and SVG `<image>` elements stay transparent and uncounted
4101
+ * because the capture gives no way to tell whether the page displayed them,
4102
+ * and counting a broken one would tell the agent to ignore a real bug.
4103
+ */
4104
+ function createUncapturedImageTracker(root, options) {
4105
+ let renderedBoxes;
4106
+ let count = 0;
4107
+ return {
4108
+ placeholderImage(cloned) {
4109
+ if (!cloned || !("currentSrc" in cloned)) return TRANSPARENT_PLACEHOLDER;
4110
+ renderedBoxes ??= collectRenderedBoxes(root, options.crop);
4111
+ const rendered = renderedBoxes.get(cloned.currentSrc || cloned.src);
4112
+ if (!rendered) return TRANSPARENT_PLACEHOLDER;
4113
+ const box = rendered.size;
4114
+ if (rendered.inCrop) count++;
4115
+ return uncapturedPlaceholder(box, {
4116
+ label: options.labelPlaceholders && labelFits(box, options.scale),
4117
+ scale: options.scale
4118
+ });
4119
+ },
4120
+ /** Distinct image URLs that were hatched inside the captured area. */
4121
+ get count() {
4122
+ return count;
4123
+ }
4124
+ };
4125
+ }
4126
+ //#endregion
4007
4127
  //#region src/edit-mode/screenshot-handler.ts
4008
4128
  let cleanupCallback$1 = void 0;
4009
- const MAX_SCREENSHOT_DIMENSION = 1568;
4129
+ const MAX_SCREENSHOT_DIMENSION_HIGH = 1568;
4130
+ const MAX_SCREENSHOT_DIMENSION_LOW = 504;
4131
+ /**
4132
+ * Resolve a {@link ScreenshotDetail} tier to its max-dimension ceiling. Both
4133
+ * tiers are hard caps -- there is no path past MAX_SCREENSHOT_DIMENSION_HIGH
4134
+ * regardless of what a caller requests.
4135
+ */
4136
+ function resolveMaxDimension(detail) {
4137
+ return detail === "high" ? MAX_SCREENSHOT_DIMENSION_HIGH : MAX_SCREENSHOT_DIMENSION_LOW;
4138
+ }
4010
4139
  const SCREENSHOT_WEBP_QUALITY = .8;
4011
4140
  const MAX_SCREENSHOT_BYTES = 256 * 1024;
4012
4141
  /** Decreasing webp qualities tried, in order, to fit {@link MAX_SCREENSHOT_BYTES}. */
@@ -4026,10 +4155,10 @@ const CHECKPOINT_MAX_PIXEL_RATIO = 2;
4026
4155
  * width resolution, because the cap applies to the longest edge. Cropping the
4027
4156
  * capture to the visible area would lift that.
4028
4157
  */
4029
- function screenshotLimits(purpose) {
4158
+ function screenshotLimits(purpose, detail) {
4030
4159
  if (purpose !== "checkpoint") return {
4031
4160
  maxBytes: MAX_SCREENSHOT_BYTES,
4032
- maxDimension: MAX_SCREENSHOT_DIMENSION,
4161
+ maxDimension: resolveMaxDimension(detail),
4033
4162
  pixelRatio: 1,
4034
4163
  qualityLadder: WEBP_QUALITY_LADDER
4035
4164
  };
@@ -4070,13 +4199,113 @@ async function enforceByteBudget(dataUrl, reencode, options) {
4070
4199
  * (for the model, Anthropic's many-image per-image limit of 2000px).
4071
4200
  */
4072
4201
  function calculateScreenshotScale(elementWidth, elementHeight, limits = {
4073
- maxDimension: MAX_SCREENSHOT_DIMENSION,
4202
+ maxDimension: MAX_SCREENSHOT_DIMENSION_HIGH,
4074
4203
  pixelRatio: 1
4075
4204
  }) {
4076
4205
  const maxOutputDim = Math.max(elementWidth, elementHeight);
4077
4206
  return Math.min(limits.pixelRatio, limits.maxDimension / maxOutputDim);
4078
4207
  }
4079
4208
  /**
4209
+ * Clamp a requested capture rectangle (`region`, APPS-5714) to the actual
4210
+ * full-image bounds. A rectangle that starts or extends past the full image
4211
+ * is shifted/truncated to fit inside it, rather than producing an invalid
4212
+ * crop; the result never collapses below 1x1.
4213
+ */
4214
+ function clampRegionToBounds(fullWidth, fullHeight, region) {
4215
+ const x = Math.min(Math.max(0, region.x), Math.max(0, fullWidth - 1));
4216
+ const y = Math.min(Math.max(0, region.y), Math.max(0, fullHeight - 1));
4217
+ return {
4218
+ x,
4219
+ y,
4220
+ width: Math.max(1, Math.min(region.width, fullWidth - x)),
4221
+ height: Math.max(1, Math.min(region.height, fullHeight - y))
4222
+ };
4223
+ }
4224
+ /**
4225
+ * Decide which capture mode wins and, for every mode but "selector", the
4226
+ * crop rectangle to apply to a full-page capture (APPS-5714).
4227
+ *
4228
+ * Precedence when a caller sets more than one of `selector`/`region`/
4229
+ * `fullPage`: selector > region > fullPage > the viewport-bound default. The
4230
+ * server tool's Zod schema is where a caller gets a real validation error for
4231
+ * setting more than one -- this resolves deterministically rather than
4232
+ * erroring, since the browser side has no channel back to the caller for
4233
+ * that kind of error.
4234
+ *
4235
+ * The viewport-bound default crops from the caller's current scroll
4236
+ * position, not the top of the page -- otherwise a scrolled preview would
4237
+ * silently show the wrong content for the cheapest, most common capture.
4238
+ *
4239
+ * Also decides `renderScale`: the scale to render the whole page at so
4240
+ * `crop` comes out at the target resolution directly, instead of always
4241
+ * rendering at native (1) and cropping/downscaling afterward. Computing
4242
+ * this from `crop` rather than the full page means the common cases
4243
+ * (default viewport, fullPage, most regions) render the page already
4244
+ * downscaled -- only a crop that itself needs more detail than a
4245
+ * downscaled render could provide still renders at native.
4246
+ */
4247
+ function resolveCaptureMode(params, fullPageDimensions, viewportDimensions, limits) {
4248
+ if (params.selector) return { mode: "selector" };
4249
+ let mode;
4250
+ let crop;
4251
+ if (params.region) {
4252
+ mode = "region";
4253
+ crop = clampRegionToBounds(fullPageDimensions.width, fullPageDimensions.height, params.region);
4254
+ } else if (params.fullPage) {
4255
+ mode = "fullPage";
4256
+ crop = {
4257
+ x: 0,
4258
+ y: 0,
4259
+ width: fullPageDimensions.width,
4260
+ height: fullPageDimensions.height
4261
+ };
4262
+ } else {
4263
+ mode = "viewport";
4264
+ crop = clampRegionToBounds(fullPageDimensions.width, fullPageDimensions.height, {
4265
+ x: viewportDimensions.scrollX,
4266
+ y: viewportDimensions.scrollY,
4267
+ width: viewportDimensions.width,
4268
+ height: viewportDimensions.height
4269
+ });
4270
+ }
4271
+ return {
4272
+ mode,
4273
+ crop,
4274
+ renderScale: calculateScreenshotScale(crop.width, crop.height, limits)
4275
+ };
4276
+ }
4277
+ /**
4278
+ * One line of capture geometry for logs. A failure inside the render or crop
4279
+ * otherwise reports only an error message, with nothing to say which mode,
4280
+ * crop or scale was in play.
4281
+ */
4282
+ function describeCaptureForLog(capture) {
4283
+ const parts = [];
4284
+ if (capture.mode) parts.push(`mode=${capture.mode}`);
4285
+ if (capture.pageWidth !== void 0 && capture.pageHeight !== void 0) parts.push(`page=${capture.pageWidth}x${capture.pageHeight}`);
4286
+ if (capture.crop) {
4287
+ const { x, y, width, height } = capture.crop;
4288
+ parts.push(`crop=${width}x${height}@(${x},${y})`);
4289
+ }
4290
+ if (capture.renderScale !== void 0) parts.push(`renderScale=${capture.renderScale.toFixed(2)}`);
4291
+ return parts.length > 0 ? parts.join(", ") : "geometry not resolved yet";
4292
+ }
4293
+ /**
4294
+ * Measure the root in the same coordinate space domToWebp renders it in:
4295
+ * modern-screenshot sizes its bitmap from the node's getBoundingClientRect()
4296
+ * when no explicit width/height is passed. Crop rectangles and the
4297
+ * pageWidth/pageHeight reported to the model must use this space, not the
4298
+ * document's scroll extent -- otherwise a crop computed against a larger
4299
+ * extent reads past the rendered image and comes back blank or shifted.
4300
+ */
4301
+ function measureCaptureRootDimensions(rootElement) {
4302
+ const { width, height } = rootElement.getBoundingClientRect();
4303
+ return {
4304
+ width,
4305
+ height
4306
+ };
4307
+ }
4308
+ /**
4080
4309
  * Measure the full content dimensions for a target element, accounting for
4081
4310
  * cases where scrolling happens in a nested child rather than the target
4082
4311
  * itself. When the target is the root element its own scrollHeight may only
@@ -4122,8 +4351,30 @@ async function reencodeWebp(dataUrl, { quality, scale = 1 }) {
4122
4351
  return canvas.toDataURL("image/webp", quality);
4123
4352
  }
4124
4353
  /**
4354
+ * Crop a source image to a rectangle and rescale in one pass, via the
4355
+ * 9-argument canvas `drawImage` source+dest rectangle form (APPS-5714).
4356
+ *
4357
+ * `modern-screenshot` has no crop/clip concept of its own -- its `width`/
4358
+ * `height` options force the target node's own CSS size before rendering,
4359
+ * which reflows the page rather than cropping it -- so this is built on top,
4360
+ * the same way {@link reencodeWebp} builds a resize on top of a plain canvas
4361
+ * redraw.
4362
+ */
4363
+ async function cropAndScale(dataUrl, crop, { quality, scale = 1 }) {
4364
+ const img = await loadImage(dataUrl);
4365
+ const destWidth = Math.max(1, Math.round(crop.width * scale));
4366
+ const destHeight = Math.max(1, Math.round(crop.height * scale));
4367
+ const canvas = document.createElement("canvas");
4368
+ canvas.width = destWidth;
4369
+ canvas.height = destHeight;
4370
+ const ctx = canvas.getContext("2d");
4371
+ if (!ctx) throw new Error("Canvas 2d context not available");
4372
+ ctx.drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, destWidth, destHeight);
4373
+ return canvas.toDataURL("image/webp", quality);
4374
+ }
4375
+ /**
4125
4376
  * Enforce the model's hard limits on a captured screenshot: first the pixel
4126
- * dimension cap (see {@link MAX_SCREENSHOT_DIMENSION}), then the byte budget
4377
+ * dimension cap (see {@link resolveMaxDimension}), then the byte budget
4127
4378
  * (oversized payloads stall the plan-generation stream). Returns the original
4128
4379
  * when it already fits both.
4129
4380
  */
@@ -4211,18 +4462,24 @@ function initializeScreenshotHandler() {
4211
4462
  const handleCaptureScreenshot = async ({ data }) => {
4212
4463
  const handlerStartTime = performance.now();
4213
4464
  const LOG_PREFIX = "[Library Screenshot Handler]";
4214
- const { callbackId, purpose, selector } = data.payload;
4215
- const limits = screenshotLimits(purpose);
4216
- console.log(`${LOG_PREFIX} Received capture request, callbackId=${callbackId}${selector ? `, selector="${selector}"` : ""}`);
4465
+ const { callbackId, purpose, selector, region, fullPage, detail } = data.payload;
4466
+ const limits = screenshotLimits(purpose, detail);
4467
+ console.log(`${LOG_PREFIX} Received capture request, callbackId=${callbackId}${selector ? `, selector="${selector}"` : ""}${region ? `, region=${JSON.stringify(region)}` : ""}${fullPage ? `, fullPage=true` : ""}${detail ? `, detail="${detail}"` : ""}`);
4468
+ let mode;
4469
+ let pageWidth;
4470
+ let pageHeight;
4471
+ let capturedArea;
4472
+ let capturedRenderScale;
4217
4473
  try {
4218
4474
  const importStartTime = performance.now();
4219
- const { domToWebp } = await import("modern-screenshot");
4475
+ const { createContext, domToWebp } = await import("modern-screenshot");
4220
4476
  const importMs = performance.now() - importStartTime;
4221
4477
  console.log(`${LOG_PREFIX} Dynamic import of modern-screenshot took ${importMs.toFixed(1)}ms`);
4222
4478
  const rootElement = document.getElementById("root");
4223
4479
  if (!rootElement) throw new Error("Root element not found");
4224
- let targetElement = rootElement;
4225
- let targetLabel = "root";
4480
+ let dataUrl;
4481
+ let captureMs;
4482
+ let uncapturedImageTracker;
4226
4483
  if (selector) {
4227
4484
  const matched = rootElement.querySelector(selector);
4228
4485
  const selectorError = !matched ? `Selector "${selector}" did not match any element` : matched.offsetWidth === 0 || matched.offsetHeight === 0 ? `Selector "${selector}" matched an element with no visible dimensions (${matched.offsetWidth}x${matched.offsetHeight})` : null;
@@ -4231,38 +4488,106 @@ function initializeScreenshotHandler() {
4231
4488
  editorBridge.sendCaptureScreenshotResponse(callbackId, void 0, selectorError);
4232
4489
  return;
4233
4490
  }
4234
- targetElement = matched;
4235
- targetLabel = `"${selector}"`;
4236
- console.log(`${LOG_PREFIX} Selector "${selector}" matched element (${matched.offsetWidth}x${matched.offsetHeight})`);
4237
- }
4238
- const { width: elementWidth, height: elementHeight } = measureContentDimensions(targetElement, rootElement);
4239
- const scale = calculateScreenshotScale(elementWidth, elementHeight, limits);
4240
- const captureStartTime = performance.now();
4241
- console.log(`${LOG_PREFIX} Starting domToWebp capture of ${targetLabel} (element: ${elementWidth}x${elementHeight}, scale: ${scale.toFixed(2)}, output: ~${Math.round(elementWidth * scale)}x${Math.round(elementHeight * scale)})...`);
4242
- const dataUrl = await domToWebp(targetElement, {
4243
- backgroundColor: "#ffffff",
4244
- scale,
4245
- quality: SCREENSHOT_WEBP_QUALITY,
4246
- ...selector && { onCloneNode: (cloned) => {
4247
- if (cloned instanceof HTMLElement) {
4248
- const computed = getComputedStyle(targetElement);
4249
- for (let i = 0; i < computed.length; i++) {
4250
- const prop = computed[i];
4251
- cloned.style.setProperty(prop, computed.getPropertyValue(prop));
4491
+ const targetElement = matched;
4492
+ mode = "selector";
4493
+ const { width: elementWidth, height: elementHeight } = measureContentDimensions(targetElement, rootElement);
4494
+ const scale = calculateScreenshotScale(elementWidth, elementHeight, limits);
4495
+ const uncapturedImages = createUncapturedImageTracker(targetElement, {
4496
+ labelPlaceholders: purpose !== "checkpoint",
4497
+ scale
4498
+ });
4499
+ uncapturedImageTracker = uncapturedImages;
4500
+ const captureStartTime = performance.now();
4501
+ console.log(`${LOG_PREFIX} Starting domToWebp capture of "${selector}" (element: ${elementWidth}x${elementHeight}, scale: ${scale.toFixed(2)}, output: ~${Math.round(elementWidth * scale)}x${Math.round(elementHeight * scale)})...`);
4502
+ const context = await createContext(targetElement, {
4503
+ type: "image/webp",
4504
+ autoDestruct: true,
4505
+ backgroundColor: "#ffffff",
4506
+ scale,
4507
+ quality: SCREENSHOT_WEBP_QUALITY,
4508
+ onCloneNode: (cloned) => {
4509
+ if (cloned instanceof HTMLElement) {
4510
+ const computed = getComputedStyle(targetElement);
4511
+ for (let i = 0; i < computed.length; i++) {
4512
+ const prop = computed[i];
4513
+ cloned.style.setProperty(prop, computed.getPropertyValue(prop));
4514
+ }
4252
4515
  }
4253
4516
  }
4254
- } }
4255
- });
4256
- const captureMs = performance.now() - captureStartTime;
4257
- console.log(`${LOG_PREFIX} domToWebp completed in ${captureMs.toFixed(1)}ms, output size: ${(dataUrlByteLength(dataUrl) / 1024).toFixed(1)}KB`);
4517
+ });
4518
+ context.fetch.placeholderImage = uncapturedImages.placeholderImage;
4519
+ dataUrl = await domToWebp(context);
4520
+ captureMs = performance.now() - captureStartTime;
4521
+ console.log(`${LOG_PREFIX} domToWebp completed in ${captureMs.toFixed(1)}ms, output size: ${(dataUrlByteLength(dataUrl) / 1024).toFixed(1)}KB`);
4522
+ } else {
4523
+ const { width: measuredPageWidth, height: measuredPageHeight } = measureCaptureRootDimensions(rootElement);
4524
+ pageWidth = measuredPageWidth;
4525
+ pageHeight = measuredPageHeight;
4526
+ const resolved = resolveCaptureMode({
4527
+ region,
4528
+ fullPage
4529
+ }, {
4530
+ width: pageWidth,
4531
+ height: pageHeight
4532
+ }, {
4533
+ width: window.innerWidth,
4534
+ height: window.innerHeight,
4535
+ scrollX: window.scrollX,
4536
+ scrollY: window.scrollY
4537
+ }, limits);
4538
+ mode = resolved.mode;
4539
+ const crop = resolved.crop;
4540
+ capturedArea = crop;
4541
+ const renderScale = resolved.renderScale;
4542
+ capturedRenderScale = renderScale;
4543
+ const uncapturedImages = createUncapturedImageTracker(rootElement, {
4544
+ labelPlaceholders: purpose !== "checkpoint",
4545
+ scale: renderScale,
4546
+ crop
4547
+ });
4548
+ uncapturedImageTracker = uncapturedImages;
4549
+ const captureStartTime = performance.now();
4550
+ console.log(`${LOG_PREFIX} Starting full-page domToWebp capture (mode=${mode}, page: ${pageWidth}x${pageHeight}, crop: ${crop.width}x${crop.height} @ (${crop.x},${crop.y}), renderScale: ${renderScale.toFixed(2)})...`);
4551
+ const context = await createContext(rootElement, {
4552
+ type: "image/webp",
4553
+ autoDestruct: true,
4554
+ backgroundColor: "#ffffff",
4555
+ scale: renderScale,
4556
+ quality: SCREENSHOT_WEBP_QUALITY
4557
+ });
4558
+ context.fetch.placeholderImage = uncapturedImages.placeholderImage;
4559
+ const fullDataUrl = await domToWebp(context);
4560
+ captureMs = performance.now() - captureStartTime;
4561
+ console.log(`${LOG_PREFIX} domToWebp completed in ${captureMs.toFixed(1)}ms, output size: ${(dataUrlByteLength(fullDataUrl) / 1024).toFixed(1)}KB`);
4562
+ dataUrl = await cropAndScale(fullDataUrl, {
4563
+ x: crop.x * renderScale,
4564
+ y: crop.y * renderScale,
4565
+ width: crop.width * renderScale,
4566
+ height: crop.height * renderScale
4567
+ }, { quality: SCREENSHOT_WEBP_QUALITY });
4568
+ }
4258
4569
  const safeDataUrl = await ensureWithinLimits(dataUrl, limits);
4570
+ const uncapturedImageCount = uncapturedImageTracker.count || void 0;
4571
+ if (uncapturedImageCount) console.warn(`${LOG_PREFIX} ${uncapturedImageCount} image(s) could not be drawn into the capture`);
4259
4572
  const totalMs = performance.now() - handlerStartTime;
4260
4573
  console.log(`${LOG_PREFIX} Screenshot captured successfully! Total: ${totalMs.toFixed(1)}ms (import: ${importMs.toFixed(1)}ms, capture: ${captureMs.toFixed(1)}ms)`);
4261
- editorBridge.sendCaptureScreenshotResponse(callbackId, safeDataUrl);
4574
+ editorBridge.sendCaptureScreenshotResponse(callbackId, safeDataUrl, void 0, {
4575
+ pageWidth,
4576
+ pageHeight,
4577
+ mode,
4578
+ crop: capturedArea,
4579
+ uncapturedImageCount
4580
+ });
4262
4581
  } catch (error) {
4263
4582
  const errorMessage = error instanceof Error ? error.message : String(error);
4264
4583
  const totalMs = performance.now() - handlerStartTime;
4265
- console.error(`${LOG_PREFIX} Error after ${totalMs.toFixed(1)}ms:`, errorMessage, error);
4584
+ console.error(`${LOG_PREFIX} Error after ${totalMs.toFixed(1)}ms (${describeCaptureForLog({
4585
+ mode,
4586
+ pageWidth,
4587
+ pageHeight,
4588
+ crop: capturedArea,
4589
+ renderScale: capturedRenderScale
4590
+ })}):`, errorMessage, error);
4266
4591
  editorBridge.sendCaptureScreenshotResponse(callbackId, void 0, errorMessage);
4267
4592
  }
4268
4593
  };
@@ -4411,6 +4736,7 @@ if (isEditMode()) {
4411
4736
  });
4412
4737
  try {
4413
4738
  await source_update_api_default.connect({
4739
+ socketPath: data.payload.devServerSocketPath,
4414
4740
  peerId: data.payload.peerId,
4415
4741
  userId: data.payload.userId,
4416
4742
  authorization: data.payload.devServerAuthorization,