@superblocksteam/library 2.0.167-next.0 → 2.0.168-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/jsx-dev-runtime/index.js +2 -2
- package/dist/{jsx-wrapper-1kyr6-x3.js → jsx-wrapper-CcDmmMUB.js} +2 -2
- package/dist/{jsx-wrapper-1kyr6-x3.js.map → jsx-wrapper-CcDmmMUB.js.map} +1 -1
- package/dist/lib/index.d.ts +56 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +367 -41
- package/dist/lib/index.js.map +1 -1
- package/dist/{root-store-BYYXPQRA.js → root-store-B4NzJHt8.js} +20 -9
- package/dist/root-store-B4NzJHt8.js.map +1 -0
- package/dist/{sdk-api-discovery-DvvsdT1z.js → sdk-api-discovery-D6Xp39R4.js} +2 -2
- package/dist/{sdk-api-discovery-DvvsdT1z.js.map → sdk-api-discovery-D6Xp39R4.js.map} +1 -1
- package/package.json +5 -5
- package/dist/root-store-BYYXPQRA.js.map +0 -1
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
4225
|
-
let
|
|
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
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
scale,
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
-
|
|
4257
|
-
|
|
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
|
|
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,
|