@insitue/sdk 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture-only.js +2 -2
- package/dist/{chunk-YUNYW2IC.js → chunk-PRCHVT5A.js} +80 -1
- package/dist/{chunk-X7V2UEBO.js → chunk-RYS5Z2BU.js} +348 -54
- package/dist/{chunk-VMBBJKFF.js → chunk-VWPAKOUW.js} +97 -1
- package/dist/index.js +3 -3
- package/dist/overlay.js +2 -2
- package/package.json +1 -1
package/dist/capture-only.js
CHANGED
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
d,
|
|
7
7
|
installRuntimeCollectors,
|
|
8
8
|
k,
|
|
9
|
+
onDisplayMediaChange,
|
|
10
|
+
retryDisplayMedia,
|
|
11
|
+
stopDisplayMedia,
|
|
9
12
|
y
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-RYS5Z2BU.js";
|
|
11
14
|
|
|
12
15
|
// src/capture-only.ts
|
|
13
16
|
var DEFAULT_INGEST = "https://www.insitue.com/api/v1/capture";
|
|
@@ -68,6 +71,32 @@ function CaptureOnlyApp(props) {
|
|
|
68
71
|
const [phase, setPhase] = d("idle");
|
|
69
72
|
const [bundle, setBundle] = d(null);
|
|
70
73
|
const [note, setNote] = d("");
|
|
74
|
+
const [tabCaptureActive, setTabCaptureActive] = d(false);
|
|
75
|
+
const [tabCaptureDenied, setTabCaptureDenied] = d(false);
|
|
76
|
+
y(() => {
|
|
77
|
+
return onDisplayMediaChange((active, reason) => {
|
|
78
|
+
setTabCaptureActive(active);
|
|
79
|
+
if (reason === "denied") setTabCaptureDenied(true);
|
|
80
|
+
if (reason === "granted") setTabCaptureDenied(false);
|
|
81
|
+
});
|
|
82
|
+
}, []);
|
|
83
|
+
const retryWithPixelPerfect = async () => {
|
|
84
|
+
if (!bundle?.target?.selector) {
|
|
85
|
+
await retryDisplayMedia();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const granted = await retryDisplayMedia();
|
|
89
|
+
if (granted) {
|
|
90
|
+
setPhase("picking");
|
|
91
|
+
const sel = await beginPick("element").catch(() => null);
|
|
92
|
+
if (sel) {
|
|
93
|
+
setBundle(await buildBundle(sel));
|
|
94
|
+
setPhase("compose");
|
|
95
|
+
} else {
|
|
96
|
+
setPhase("compose");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
71
100
|
const sink = new IssueTrackerSink(async (draft) => {
|
|
72
101
|
globalThis.__insitu_capture__ = {
|
|
73
102
|
title: draft.title,
|
|
@@ -235,6 +264,56 @@ function CaptureOnlyApp(props) {
|
|
|
235
264
|
},
|
|
236
265
|
"Screenshot unavailable \u2014 sending the rest."
|
|
237
266
|
) : null,
|
|
267
|
+
// Nudge: the screenshot is structurally OK but some content
|
|
268
|
+
// couldn't be embedded (non-CORS img / video / canvas). Offer
|
|
269
|
+
// a one-tap upgrade to pixel-perfect mode + re-pick.
|
|
270
|
+
bundle?.screenshot?.qualityNote && !tabCaptureActive ? k(
|
|
271
|
+
"div",
|
|
272
|
+
{
|
|
273
|
+
style: `display:flex;align-items:center;gap:8px;padding:9px 11px;background:#fff7ed;border:1px solid #fbd9b1;color:#8a4b00;border-radius:10px;font-size:12px;margin-bottom:12px`
|
|
274
|
+
},
|
|
275
|
+
[
|
|
276
|
+
k(
|
|
277
|
+
"span",
|
|
278
|
+
{ style: "flex:1" },
|
|
279
|
+
"Some content didn't capture cleanly. Enable tab capture for a pixel-perfect screenshot."
|
|
280
|
+
),
|
|
281
|
+
k(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
onClick: () => void retryWithPixelPerfect(),
|
|
285
|
+
style: `all:unset;cursor:pointer;color:#5751e6;font-weight:600;padding:2px 8px;border:1px solid #c5c2ff;border-radius:6px;background:#fff`
|
|
286
|
+
},
|
|
287
|
+
"Enable"
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
) : null,
|
|
291
|
+
// Active badge — tells the user why the browser's red "tab
|
|
292
|
+
// sharing" indicator is on + lets them stop it.
|
|
293
|
+
tabCaptureActive ? k(
|
|
294
|
+
"div",
|
|
295
|
+
{
|
|
296
|
+
style: `display:flex;align-items:center;gap:8px;padding:7px 11px;background:#ecfdf5;border:1px solid #b6e6cf;color:#117a52;border-radius:10px;font-size:11.5px;margin-bottom:12px`
|
|
297
|
+
},
|
|
298
|
+
[
|
|
299
|
+
k("span", {
|
|
300
|
+
style: "width:8px;height:8px;border-radius:50%;background:#117a52;box-shadow:0 0 6px #117a52"
|
|
301
|
+
}),
|
|
302
|
+
k(
|
|
303
|
+
"span",
|
|
304
|
+
{ style: "flex:1" },
|
|
305
|
+
"Tab capture active \u2014 screenshots are pixel-perfect."
|
|
306
|
+
),
|
|
307
|
+
k(
|
|
308
|
+
"button",
|
|
309
|
+
{
|
|
310
|
+
onClick: () => stopDisplayMedia("user"),
|
|
311
|
+
style: `all:unset;cursor:pointer;color:#117a52;font-weight:600;padding:2px 6px`
|
|
312
|
+
},
|
|
313
|
+
"Stop"
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
) : null,
|
|
238
317
|
k("textarea", {
|
|
239
318
|
value: note,
|
|
240
319
|
rows: 3,
|
|
@@ -582,7 +582,7 @@ function resolveTarget(el) {
|
|
|
582
582
|
}
|
|
583
583
|
return source === void 0 ? { confidence, componentStack, selector } : { source, confidence, componentStack, selector };
|
|
584
584
|
}
|
|
585
|
-
var CAPTURE_SCHEMA_VERSION =
|
|
585
|
+
var CAPTURE_SCHEMA_VERSION = 3;
|
|
586
586
|
var PROTOCOL_VERSION = 4;
|
|
587
587
|
function toIssueDraft(bundle) {
|
|
588
588
|
const t3 = bundle.target;
|
|
@@ -597,7 +597,7 @@ function toIssueDraft(bundle) {
|
|
|
597
597
|
`**Viewport:** ${bundle.viewport.w}\xD7${bundle.viewport.h}${bundle.viewport.breakpoint ? ` (${bundle.viewport.breakpoint})` : ""}`,
|
|
598
598
|
`**Tailwind:** ${bundle.tailwindClasses.join(" ") || "\u2014"}`,
|
|
599
599
|
`**Runtime:** ${bundle.runtime.console.length} log \xB7 ${bundle.runtime.network.length} net \xB7 ${errs} err`,
|
|
600
|
-
`**Screenshot:** ${bundle.screenshot ?
|
|
600
|
+
`**Screenshot:** ${bundle.screenshot ? `attached` + (bundle.screenshot.source ? ` (${bundle.screenshot.source})` : "") + (bundle.screenshot.qualityNote ? ` \u2014 ${bundle.screenshot.qualityNote}` : "") : bundle.screenshotUnavailable ? `unavailable \u2014 ${bundle.screenshotUnavailable}` : "\u2014"}`,
|
|
601
601
|
bundle.userNote ? `
|
|
602
602
|
> ${bundle.userNote}` : "",
|
|
603
603
|
`
|
|
@@ -802,6 +802,53 @@ function beginPick(mode = "element") {
|
|
|
802
802
|
});
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
+
// src/capture-settings.ts
|
|
806
|
+
var DEFAULT_SETTINGS = {
|
|
807
|
+
alwaysPixelPerfect: false
|
|
808
|
+
};
|
|
809
|
+
function storageKey() {
|
|
810
|
+
if (typeof location === "undefined") return "insitu:capture-settings";
|
|
811
|
+
return `insitu:capture-settings:${location.host}`;
|
|
812
|
+
}
|
|
813
|
+
var cached = null;
|
|
814
|
+
function getCaptureSettings() {
|
|
815
|
+
if (cached) return cached;
|
|
816
|
+
if (typeof localStorage === "undefined") {
|
|
817
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const raw = localStorage.getItem(storageKey());
|
|
822
|
+
if (!raw) {
|
|
823
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
824
|
+
return cached;
|
|
825
|
+
}
|
|
826
|
+
const parsed = JSON.parse(raw);
|
|
827
|
+
cached = { ...DEFAULT_SETTINGS, ...parsed };
|
|
828
|
+
return cached;
|
|
829
|
+
} catch {
|
|
830
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
831
|
+
return cached;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function setCaptureSettings(patch) {
|
|
835
|
+
const next = { ...getCaptureSettings(), ...patch };
|
|
836
|
+
cached = next;
|
|
837
|
+
if (typeof localStorage !== "undefined") {
|
|
838
|
+
try {
|
|
839
|
+
localStorage.setItem(storageKey(), JSON.stringify(next));
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const l3 of listeners) l3(next);
|
|
844
|
+
}
|
|
845
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
846
|
+
function onCaptureSettingsChange(l3) {
|
|
847
|
+
listeners.add(l3);
|
|
848
|
+
l3(getCaptureSettings());
|
|
849
|
+
return () => listeners.delete(l3);
|
|
850
|
+
}
|
|
851
|
+
|
|
805
852
|
// ../../node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/util.js
|
|
806
853
|
function resolveUrl(url, baseUrl) {
|
|
807
854
|
if (url.match(/^[a-z]+:\/\//i)) {
|
|
@@ -1604,30 +1651,9 @@ function crossOrigin(url) {
|
|
|
1604
1651
|
return false;
|
|
1605
1652
|
}
|
|
1606
1653
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
if (el instanceof HTMLImageElement) {
|
|
1611
|
-
if (crossOrigin(el.currentSrc || el.src) && el.crossOrigin == null) {
|
|
1612
|
-
const host = (() => {
|
|
1613
|
-
try {
|
|
1614
|
-
return new URL(el.currentSrc || el.src).host;
|
|
1615
|
-
} catch {
|
|
1616
|
-
return "cross-origin";
|
|
1617
|
-
}
|
|
1618
|
-
})();
|
|
1619
|
-
return `cross-origin <img> (${host})`;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
const bg = getComputedStyle(el).backgroundImage;
|
|
1623
|
-
const m3 = bg && bg !== "none" ? /url\(["']?([^"')]+)["']?\)/.exec(bg) : null;
|
|
1624
|
-
if (m3 && crossOrigin(m3[1])) return "cross-origin CSS background image";
|
|
1625
|
-
if ((el instanceof HTMLVideoElement || el instanceof HTMLCanvasElement) && crossOrigin(el.src)) {
|
|
1626
|
-
return `cross-origin <${el.tagName.toLowerCase()}>`;
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
return null;
|
|
1630
|
-
}
|
|
1654
|
+
var IMAGE_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1655
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" fill="#e8e8e8"/><path d="M0 0 L32 32 M32 0 L0 32" stroke="#b0b0b0" stroke-width="1.5"/></svg>`
|
|
1656
|
+
);
|
|
1631
1657
|
function findContextAncestor(el) {
|
|
1632
1658
|
const minW = 420;
|
|
1633
1659
|
const minH = 140;
|
|
@@ -1653,17 +1679,10 @@ async function renderViewportCrop(cropRect, pixelRatio) {
|
|
|
1653
1679
|
pixelRatio,
|
|
1654
1680
|
cacheBust: true,
|
|
1655
1681
|
backgroundColor,
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
if (n2 instanceof HTMLImageElement) {
|
|
1661
|
-
if (crossOrigin(n2.currentSrc || n2.src) && n2.crossOrigin == null) {
|
|
1662
|
-
return false;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
return true;
|
|
1666
|
-
}
|
|
1682
|
+
imagePlaceholder: IMAGE_PLACEHOLDER,
|
|
1683
|
+
// Only filter out our own overlay layers — leave cross-origin
|
|
1684
|
+
// <img>s in place so embedImages can fetch+inline them.
|
|
1685
|
+
filter: (n2) => !(n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]"))
|
|
1667
1686
|
});
|
|
1668
1687
|
const sx = window.scrollX;
|
|
1669
1688
|
const sy = window.scrollY;
|
|
@@ -1683,8 +1702,240 @@ async function renderViewportCrop(cropRect, pixelRatio) {
|
|
|
1683
1702
|
out.width,
|
|
1684
1703
|
out.height
|
|
1685
1704
|
);
|
|
1705
|
+
if (looksBlankUniform(ctx, out.width, out.height)) {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1686
1708
|
return out.toDataURL("image/png");
|
|
1687
1709
|
}
|
|
1710
|
+
function looksBlankUniform(ctx, w3, h3) {
|
|
1711
|
+
if (w3 < 4 || h3 < 4) return false;
|
|
1712
|
+
const samples = [];
|
|
1713
|
+
for (let i3 = 0; i3 < 4; i3++) {
|
|
1714
|
+
for (let j3 = 0; j3 < 4; j3++) {
|
|
1715
|
+
const x2 = Math.floor(w3 * (i3 + 0.5) / 4);
|
|
1716
|
+
const y3 = Math.floor(h3 * (j3 + 0.5) / 4);
|
|
1717
|
+
try {
|
|
1718
|
+
const px2 = ctx.getImageData(x2, y3, 1, 1).data;
|
|
1719
|
+
samples.push(`${px2[0]},${px2[1]},${px2[2]},${px2[3]}`);
|
|
1720
|
+
} catch {
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return new Set(samples).size === 1;
|
|
1726
|
+
}
|
|
1727
|
+
function assessCaptureQuality(cropRect) {
|
|
1728
|
+
const out = {
|
|
1729
|
+
unembeddableImages: 0,
|
|
1730
|
+
hasVideo: false,
|
|
1731
|
+
hasCanvas: false
|
|
1732
|
+
};
|
|
1733
|
+
const all = document.querySelectorAll("img, video, canvas");
|
|
1734
|
+
for (const el of all) {
|
|
1735
|
+
if (el instanceof Element && el.closest?.("#insitu-root, [data-insitu-layer]")) {
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const r3 = el.getBoundingClientRect();
|
|
1739
|
+
const overlaps = r3.right >= cropRect.x && r3.left <= cropRect.x + cropRect.width && r3.bottom >= cropRect.y && r3.top <= cropRect.y + cropRect.height;
|
|
1740
|
+
if (!overlaps) continue;
|
|
1741
|
+
if (el instanceof HTMLImageElement) {
|
|
1742
|
+
const src = el.currentSrc || el.src;
|
|
1743
|
+
if (!src) continue;
|
|
1744
|
+
const browserLoaded = el.complete && el.naturalWidth > 0;
|
|
1745
|
+
const corsSafe = !crossOrigin(src) || el.crossOrigin != null;
|
|
1746
|
+
if (!browserLoaded || !corsSafe) {
|
|
1747
|
+
out.unembeddableImages++;
|
|
1748
|
+
}
|
|
1749
|
+
} else if (el instanceof HTMLVideoElement) {
|
|
1750
|
+
if (r3.width > 0 && r3.height > 0) out.hasVideo = true;
|
|
1751
|
+
} else if (el instanceof HTMLCanvasElement) {
|
|
1752
|
+
if (r3.width > 0 && r3.height > 0) out.hasCanvas = true;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
function describeImperfection(q2) {
|
|
1758
|
+
const parts = [];
|
|
1759
|
+
if (q2.unembeddableImages > 0) {
|
|
1760
|
+
parts.push(
|
|
1761
|
+
`${q2.unembeddableImages} non-CORS image${q2.unembeddableImages > 1 ? "s" : ""}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
if (q2.hasVideo) parts.push("video frame");
|
|
1765
|
+
if (q2.hasCanvas) parts.push("canvas content");
|
|
1766
|
+
return parts.join(" + ");
|
|
1767
|
+
}
|
|
1768
|
+
var displayMediaState = {
|
|
1769
|
+
stream: null,
|
|
1770
|
+
trackEndedHandler: null,
|
|
1771
|
+
idleTimer: null,
|
|
1772
|
+
deniedAt: null
|
|
1773
|
+
};
|
|
1774
|
+
var IDLE_MS = 9e4;
|
|
1775
|
+
var displayMediaListeners = /* @__PURE__ */ new Set();
|
|
1776
|
+
function onDisplayMediaChange(l3) {
|
|
1777
|
+
displayMediaListeners.add(l3);
|
|
1778
|
+
l3(displayMediaState.stream != null);
|
|
1779
|
+
return () => displayMediaListeners.delete(l3);
|
|
1780
|
+
}
|
|
1781
|
+
function notifyDisplayMedia(reason) {
|
|
1782
|
+
const active = displayMediaState.stream != null;
|
|
1783
|
+
for (const l3 of displayMediaListeners) l3(active, reason);
|
|
1784
|
+
}
|
|
1785
|
+
function stopDisplayMedia(reason = "stopped") {
|
|
1786
|
+
if (displayMediaState.stream) {
|
|
1787
|
+
for (const t3 of displayMediaState.stream.getTracks()) t3.stop();
|
|
1788
|
+
}
|
|
1789
|
+
if (displayMediaState.idleTimer) clearTimeout(displayMediaState.idleTimer);
|
|
1790
|
+
if (displayMediaState.trackEndedHandler && displayMediaState.stream) {
|
|
1791
|
+
for (const t3 of displayMediaState.stream.getTracks()) {
|
|
1792
|
+
t3.removeEventListener("ended", displayMediaState.trackEndedHandler);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
displayMediaState.stream = null;
|
|
1796
|
+
displayMediaState.trackEndedHandler = null;
|
|
1797
|
+
displayMediaState.idleTimer = null;
|
|
1798
|
+
notifyDisplayMedia(reason);
|
|
1799
|
+
}
|
|
1800
|
+
function bumpIdleTimer() {
|
|
1801
|
+
if (displayMediaState.idleTimer) clearTimeout(displayMediaState.idleTimer);
|
|
1802
|
+
displayMediaState.idleTimer = setTimeout(
|
|
1803
|
+
() => stopDisplayMedia("idle"),
|
|
1804
|
+
IDLE_MS
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
function supportsDisplayMedia() {
|
|
1808
|
+
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
|
|
1809
|
+
}
|
|
1810
|
+
async function ensureDisplayMediaStream() {
|
|
1811
|
+
if (!supportsDisplayMedia()) return null;
|
|
1812
|
+
if (displayMediaState.stream) {
|
|
1813
|
+
bumpIdleTimer();
|
|
1814
|
+
return displayMediaState.stream;
|
|
1815
|
+
}
|
|
1816
|
+
if (displayMediaState.deniedAt && Date.now() - displayMediaState.deniedAt < 6e4) {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
1821
|
+
// `displaySurface: 'browser'` + `preferCurrentTab: true` makes
|
|
1822
|
+
// Chrome/Edge default-select the current tab in the prompt.
|
|
1823
|
+
// Other browsers ignore the hints; user still picks manually.
|
|
1824
|
+
video: {
|
|
1825
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1826
|
+
displaySurface: "browser"
|
|
1827
|
+
},
|
|
1828
|
+
audio: false,
|
|
1829
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1830
|
+
preferCurrentTab: true
|
|
1831
|
+
});
|
|
1832
|
+
displayMediaState.stream = stream;
|
|
1833
|
+
displayMediaState.deniedAt = null;
|
|
1834
|
+
const handler = () => stopDisplayMedia("track-ended");
|
|
1835
|
+
for (const t3 of stream.getTracks()) t3.addEventListener("ended", handler);
|
|
1836
|
+
displayMediaState.trackEndedHandler = handler;
|
|
1837
|
+
bumpIdleTimer();
|
|
1838
|
+
notifyDisplayMedia("granted");
|
|
1839
|
+
return stream;
|
|
1840
|
+
} catch {
|
|
1841
|
+
displayMediaState.deniedAt = Date.now();
|
|
1842
|
+
notifyDisplayMedia("denied");
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async function retryDisplayMedia() {
|
|
1847
|
+
displayMediaState.deniedAt = null;
|
|
1848
|
+
const s3 = await ensureDisplayMediaStream();
|
|
1849
|
+
return s3 != null;
|
|
1850
|
+
}
|
|
1851
|
+
function hideOverlayLayersBriefly() {
|
|
1852
|
+
const id = "insitu-capture-hide";
|
|
1853
|
+
const style = document.createElement("style");
|
|
1854
|
+
style.id = id;
|
|
1855
|
+
style.textContent = `
|
|
1856
|
+
#insitu-root, [data-insitu-layer] { visibility: hidden !important; }
|
|
1857
|
+
`;
|
|
1858
|
+
document.head.appendChild(style);
|
|
1859
|
+
return () => {
|
|
1860
|
+
style.remove();
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
async function tryGrabViaDisplayMedia(cropRect, pixelRatio) {
|
|
1864
|
+
const wasActive = displayMediaState.stream != null;
|
|
1865
|
+
const stream = await ensureDisplayMediaStream();
|
|
1866
|
+
if (!stream) return null;
|
|
1867
|
+
const fresh = !wasActive;
|
|
1868
|
+
bumpIdleTimer();
|
|
1869
|
+
const restoreOverlay = hideOverlayLayersBriefly();
|
|
1870
|
+
await new Promise(
|
|
1871
|
+
(r3) => requestAnimationFrame(() => requestAnimationFrame(() => r3()))
|
|
1872
|
+
);
|
|
1873
|
+
try {
|
|
1874
|
+
const track = stream.getVideoTracks()[0];
|
|
1875
|
+
if (!track) return null;
|
|
1876
|
+
let bitmap = null;
|
|
1877
|
+
const Ctor = window.ImageCapture;
|
|
1878
|
+
if (Ctor) {
|
|
1879
|
+
bitmap = await new Ctor(track).grabFrame();
|
|
1880
|
+
} else {
|
|
1881
|
+
bitmap = await grabFrameViaVideo(stream);
|
|
1882
|
+
}
|
|
1883
|
+
if (!bitmap) return null;
|
|
1884
|
+
const frameW = bitmap.width;
|
|
1885
|
+
const frameH = bitmap.height;
|
|
1886
|
+
const scaleX = frameW / window.innerWidth;
|
|
1887
|
+
const scaleY = frameH / window.innerHeight;
|
|
1888
|
+
const sx = Math.max(0, Math.round(cropRect.x * scaleX));
|
|
1889
|
+
const sy = Math.max(0, Math.round(cropRect.y * scaleY));
|
|
1890
|
+
const sw = Math.min(frameW - sx, Math.round(cropRect.width * scaleX));
|
|
1891
|
+
const sh = Math.min(frameH - sy, Math.round(cropRect.height * scaleY));
|
|
1892
|
+
const out = document.createElement("canvas");
|
|
1893
|
+
out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
|
|
1894
|
+
out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
|
|
1895
|
+
const ctx = out.getContext("2d");
|
|
1896
|
+
if (!ctx) return null;
|
|
1897
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, out.width, out.height);
|
|
1898
|
+
bitmap.close?.();
|
|
1899
|
+
return { dataUrl: out.toDataURL("image/png"), fresh };
|
|
1900
|
+
} finally {
|
|
1901
|
+
restoreOverlay();
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async function grabFrameViaVideo(stream) {
|
|
1905
|
+
const video = document.createElement("video");
|
|
1906
|
+
video.srcObject = stream;
|
|
1907
|
+
video.muted = true;
|
|
1908
|
+
video.playsInline = true;
|
|
1909
|
+
video.style.position = "fixed";
|
|
1910
|
+
video.style.pointerEvents = "none";
|
|
1911
|
+
video.style.opacity = "0";
|
|
1912
|
+
video.style.width = "1px";
|
|
1913
|
+
video.style.height = "1px";
|
|
1914
|
+
document.body.appendChild(video);
|
|
1915
|
+
try {
|
|
1916
|
+
await video.play().catch(() => void 0);
|
|
1917
|
+
await new Promise((resolve, reject) => {
|
|
1918
|
+
const onReady = () => {
|
|
1919
|
+
video.removeEventListener("loadeddata", onReady);
|
|
1920
|
+
resolve();
|
|
1921
|
+
};
|
|
1922
|
+
video.addEventListener("loadeddata", onReady, { once: true });
|
|
1923
|
+
setTimeout(() => reject(new Error("video timeout")), 2e3);
|
|
1924
|
+
});
|
|
1925
|
+
const tmp = document.createElement("canvas");
|
|
1926
|
+
tmp.width = video.videoWidth;
|
|
1927
|
+
tmp.height = video.videoHeight;
|
|
1928
|
+
const ctx = tmp.getContext("2d");
|
|
1929
|
+
if (!ctx) throw new Error("no 2d ctx");
|
|
1930
|
+
ctx.drawImage(video, 0, 0);
|
|
1931
|
+
return await createImageBitmap(tmp);
|
|
1932
|
+
} finally {
|
|
1933
|
+
video.remove();
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof window !== "undefined") {
|
|
1937
|
+
window.addEventListener("pagehide", () => stopDisplayMedia("pagehide"));
|
|
1938
|
+
}
|
|
1688
1939
|
function elementFor(sel) {
|
|
1689
1940
|
if (sel.mode === "element") return sel.pointerPath?.[0] ?? null;
|
|
1690
1941
|
if (sel.rect) {
|
|
@@ -1698,6 +1949,7 @@ async function buildBundle(sel) {
|
|
|
1698
1949
|
const el = elementFor(sel);
|
|
1699
1950
|
const rt = runtimeSnapshot();
|
|
1700
1951
|
const dpr = window.devicePixelRatio || 1;
|
|
1952
|
+
const settings = getCaptureSettings();
|
|
1701
1953
|
let screenshot;
|
|
1702
1954
|
let screenshotUnavailable;
|
|
1703
1955
|
if (el instanceof HTMLElement) {
|
|
@@ -1716,32 +1968,68 @@ async function buildBundle(sel) {
|
|
|
1716
1968
|
el.style.outline = "3px solid #ff6b00";
|
|
1717
1969
|
el.style.outlineOffset = "2px";
|
|
1718
1970
|
try {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1971
|
+
const skipLayer1 = settings.alwaysPixelPerfect;
|
|
1972
|
+
let layer1Result = null;
|
|
1973
|
+
let quality = null;
|
|
1974
|
+
if (!skipLayer1) {
|
|
1975
|
+
layer1Result = await renderViewportCrop(
|
|
1976
|
+
cropRect,
|
|
1977
|
+
Math.min(dpr, 1.5)
|
|
1978
|
+
);
|
|
1979
|
+
quality = assessCaptureQuality(cropRect);
|
|
1980
|
+
}
|
|
1981
|
+
const imperfect = !layer1Result || quality != null && (quality.unembeddableImages > 0 || quality.hasVideo || quality.hasCanvas);
|
|
1982
|
+
if (imperfect || skipLayer1) {
|
|
1983
|
+
const grab = await tryGrabViaDisplayMedia(
|
|
1984
|
+
cropRect,
|
|
1985
|
+
Math.min(dpr, 2)
|
|
1986
|
+
);
|
|
1987
|
+
if (grab) {
|
|
1988
|
+
screenshot = {
|
|
1989
|
+
mime: "image/png",
|
|
1990
|
+
dataUrl: grab.dataUrl,
|
|
1991
|
+
bounds: {
|
|
1992
|
+
x: cropRect.x,
|
|
1993
|
+
y: cropRect.y,
|
|
1994
|
+
width: cropRect.width,
|
|
1995
|
+
height: cropRect.height
|
|
1996
|
+
},
|
|
1997
|
+
source: "display-media"
|
|
1998
|
+
};
|
|
1999
|
+
} else if (layer1Result) {
|
|
2000
|
+
const reason = quality ? describeImperfection(quality) : "non-CORS content";
|
|
2001
|
+
screenshot = {
|
|
2002
|
+
mime: "image/png",
|
|
2003
|
+
dataUrl: layer1Result,
|
|
2004
|
+
bounds: {
|
|
2005
|
+
x: cropRect.x,
|
|
2006
|
+
y: cropRect.y,
|
|
2007
|
+
width: cropRect.width,
|
|
2008
|
+
height: cropRect.height
|
|
2009
|
+
},
|
|
2010
|
+
source: "rasterise",
|
|
2011
|
+
qualityNote: `${reason} couldn't be embedded \u2014 grant tab capture for pixel-perfect screenshots`
|
|
2012
|
+
};
|
|
2013
|
+
} else {
|
|
2014
|
+
screenshotUnavailable = supportsDisplayMedia() ? "rasterise failed \u2014 grant tab capture for pixel-perfect screenshots" : "rasterise failed and tab capture unsupported in this browser";
|
|
2015
|
+
}
|
|
2016
|
+
} else if (layer1Result) {
|
|
1730
2017
|
screenshot = {
|
|
1731
2018
|
mime: "image/png",
|
|
1732
|
-
dataUrl,
|
|
1733
|
-
// Bounds describe the SCREENSHOT (the crop region) so
|
|
1734
|
-
// the dashboard knows what slice of viewport this is.
|
|
2019
|
+
dataUrl: layer1Result,
|
|
1735
2020
|
bounds: {
|
|
1736
2021
|
x: cropRect.x,
|
|
1737
2022
|
y: cropRect.y,
|
|
1738
2023
|
width: cropRect.width,
|
|
1739
2024
|
height: cropRect.height
|
|
1740
|
-
}
|
|
2025
|
+
},
|
|
2026
|
+
source: "rasterise"
|
|
1741
2027
|
};
|
|
2028
|
+
} else {
|
|
2029
|
+
screenshotUnavailable = "rasterise produced an empty image";
|
|
1742
2030
|
}
|
|
1743
|
-
} catch {
|
|
1744
|
-
screenshotUnavailable = "rasterise failed";
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
screenshotUnavailable = err instanceof Error ? `rasterise failed: ${err.message}` : "rasterise failed";
|
|
1745
2033
|
} finally {
|
|
1746
2034
|
el.style.outline = orig.outline;
|
|
1747
2035
|
el.style.outlineOffset = orig.outlineOffset;
|
|
@@ -1784,5 +2072,11 @@ export {
|
|
|
1784
2072
|
installRuntimeCollectors,
|
|
1785
2073
|
runtimeErrorCount,
|
|
1786
2074
|
beginPick,
|
|
2075
|
+
getCaptureSettings,
|
|
2076
|
+
setCaptureSettings,
|
|
2077
|
+
onCaptureSettingsChange,
|
|
2078
|
+
onDisplayMediaChange,
|
|
2079
|
+
stopDisplayMedia,
|
|
2080
|
+
retryDisplayMedia,
|
|
1787
2081
|
buildBundle
|
|
1788
2082
|
};
|
|
@@ -5,11 +5,17 @@ import {
|
|
|
5
5
|
beginPick,
|
|
6
6
|
buildBundle,
|
|
7
7
|
d,
|
|
8
|
+
getCaptureSettings,
|
|
8
9
|
installRuntimeCollectors,
|
|
9
10
|
k,
|
|
11
|
+
onCaptureSettingsChange,
|
|
12
|
+
onDisplayMediaChange,
|
|
13
|
+
retryDisplayMedia,
|
|
10
14
|
runtimeErrorCount,
|
|
15
|
+
setCaptureSettings,
|
|
16
|
+
stopDisplayMedia,
|
|
11
17
|
y
|
|
12
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-RYS5Z2BU.js";
|
|
13
19
|
|
|
14
20
|
// src/client.ts
|
|
15
21
|
var CompanionClient = class {
|
|
@@ -238,6 +244,11 @@ function App(props) {
|
|
|
238
244
|
const [showCtx, setShowCtx] = d(false);
|
|
239
245
|
const [showSettings, setShowSettings] = d(false);
|
|
240
246
|
const [autoApply, setAutoApply] = d(false);
|
|
247
|
+
const [captureSettings, setCaptureSettingsState] = d(
|
|
248
|
+
getCaptureSettings()
|
|
249
|
+
);
|
|
250
|
+
const [displayMediaActive, setDisplayMediaActive] = d(false);
|
|
251
|
+
const [displayMediaDenied, setDisplayMediaDenied] = d(false);
|
|
241
252
|
const [agentReady, setAgentReady] = d(null);
|
|
242
253
|
const [agentNote, setAgentNote] = d("");
|
|
243
254
|
const [chatInput, setChatInput] = d("");
|
|
@@ -433,6 +444,18 @@ function App(props) {
|
|
|
433
444
|
const el = threadRef.current;
|
|
434
445
|
if (el) el.scrollTop = el.scrollHeight;
|
|
435
446
|
}, [messages, changes, turnBusy, activity]);
|
|
447
|
+
y(() => {
|
|
448
|
+
const off1 = onDisplayMediaChange((active, reason) => {
|
|
449
|
+
setDisplayMediaActive(active);
|
|
450
|
+
if (reason === "denied") setDisplayMediaDenied(true);
|
|
451
|
+
if (reason === "granted") setDisplayMediaDenied(false);
|
|
452
|
+
});
|
|
453
|
+
const off2 = onCaptureSettingsChange((s) => setCaptureSettingsState(s));
|
|
454
|
+
return () => {
|
|
455
|
+
off1();
|
|
456
|
+
off2();
|
|
457
|
+
};
|
|
458
|
+
}, []);
|
|
436
459
|
y(() => {
|
|
437
460
|
const onKey = (ev) => {
|
|
438
461
|
const meta = ev.metaKey || ev.ctrlKey;
|
|
@@ -964,8 +987,79 @@ ${resolved.snippet}`
|
|
|
964
987
|
"div",
|
|
965
988
|
{ style: `color:${muted};margin-top:4px` },
|
|
966
989
|
"Writes proposed changes immediately. Still checkpointed & undoable; no manual gate. Resets on reload."
|
|
990
|
+
),
|
|
991
|
+
k(
|
|
992
|
+
"label",
|
|
993
|
+
{
|
|
994
|
+
style: "display:flex;gap:8px;align-items:center;cursor:pointer;color:#ececef;margin-top:10px"
|
|
995
|
+
},
|
|
996
|
+
[
|
|
997
|
+
k("input", {
|
|
998
|
+
type: "checkbox",
|
|
999
|
+
checked: captureSettings.alwaysPixelPerfect,
|
|
1000
|
+
onChange: (ev) => setCaptureSettings({
|
|
1001
|
+
alwaysPixelPerfect: ev.target.checked
|
|
1002
|
+
})
|
|
1003
|
+
}),
|
|
1004
|
+
k("span", {}, "Always pixel-perfect screenshots")
|
|
1005
|
+
]
|
|
1006
|
+
),
|
|
1007
|
+
k(
|
|
1008
|
+
"div",
|
|
1009
|
+
{ style: `color:${muted};margin-top:4px` },
|
|
1010
|
+
"Skips the silent rasterise path and always uses tab capture. One permission per session; every screenshot is OS-pixel accurate."
|
|
967
1011
|
)
|
|
968
1012
|
]) : null;
|
|
1013
|
+
const captureActivePill = displayMediaActive ? k(
|
|
1014
|
+
"div",
|
|
1015
|
+
{
|
|
1016
|
+
style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#1a2a1f;border:1px solid #2f5040;color:#9fe7b8;font-size:11px"
|
|
1017
|
+
},
|
|
1018
|
+
[
|
|
1019
|
+
k(
|
|
1020
|
+
"span",
|
|
1021
|
+
{ style: "display:inline-flex;align-items:center;gap:6px" },
|
|
1022
|
+
[
|
|
1023
|
+
k("span", {
|
|
1024
|
+
style: "width:8px;height:8px;border-radius:50%;background:#2fd16b;box-shadow:0 0 6px #2fd16b"
|
|
1025
|
+
}),
|
|
1026
|
+
k("span", {}, "Tab capture active")
|
|
1027
|
+
]
|
|
1028
|
+
),
|
|
1029
|
+
k(
|
|
1030
|
+
"button",
|
|
1031
|
+
{
|
|
1032
|
+
style: `${btn};margin-left:auto`,
|
|
1033
|
+
onClick: () => stopDisplayMedia("user"),
|
|
1034
|
+
title: "Stop sharing this tab"
|
|
1035
|
+
},
|
|
1036
|
+
"Stop"
|
|
1037
|
+
)
|
|
1038
|
+
]
|
|
1039
|
+
) : null;
|
|
1040
|
+
const captureDeniedNudge = displayMediaDenied && !displayMediaActive ? k(
|
|
1041
|
+
"div",
|
|
1042
|
+
{
|
|
1043
|
+
style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#2a1f1a;border:1px solid #5a3a2a;color:#e8c69f;font-size:11px"
|
|
1044
|
+
},
|
|
1045
|
+
[
|
|
1046
|
+
k(
|
|
1047
|
+
"span",
|
|
1048
|
+
{ style: "flex:1" },
|
|
1049
|
+
"Screenshot missed some cross-origin content. Grant tab capture for pixel-perfect captures."
|
|
1050
|
+
),
|
|
1051
|
+
k(
|
|
1052
|
+
"button",
|
|
1053
|
+
{
|
|
1054
|
+
style: btn,
|
|
1055
|
+
onClick: () => {
|
|
1056
|
+
void retryDisplayMedia();
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
"Enable"
|
|
1060
|
+
)
|
|
1061
|
+
]
|
|
1062
|
+
) : null;
|
|
969
1063
|
const panel = open ? k(
|
|
970
1064
|
"div",
|
|
971
1065
|
{
|
|
@@ -1021,6 +1115,8 @@ ${resolved.snippet}`
|
|
|
1021
1115
|
]
|
|
1022
1116
|
),
|
|
1023
1117
|
settings,
|
|
1118
|
+
captureActivePill,
|
|
1119
|
+
captureDeniedNudge,
|
|
1024
1120
|
ctx,
|
|
1025
1121
|
conversation,
|
|
1026
1122
|
timeline,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountCaptureOnly
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PRCHVT5A.js";
|
|
4
4
|
import {
|
|
5
5
|
mountInSitue
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-VWPAKOUW.js";
|
|
7
|
+
import "./chunk-RYS5Z2BU.js";
|
|
8
8
|
|
|
9
9
|
// src/InSitue.tsx
|
|
10
10
|
import { useEffect } from "react";
|
package/dist/overlay.js
CHANGED
package/package.json
CHANGED