getobsrv 0.7.0 → 0.7.2
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/out/main/index.js
CHANGED
|
@@ -157,7 +157,14 @@ function parseUiState(raw) {
|
|
|
157
157
|
if (typeof profileId !== "string" || profileId.length === 0 || profileId.length > MAX_UI_ID) return null;
|
|
158
158
|
if (viewMode !== "1:1" && viewMode !== "fit") return null;
|
|
159
159
|
if (mode !== "url" && mode !== "image") return null;
|
|
160
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
presetId,
|
|
162
|
+
profileId,
|
|
163
|
+
viewMode,
|
|
164
|
+
mode,
|
|
165
|
+
targetBounds: parseRect(raw.targetBounds),
|
|
166
|
+
canvasBounds: parseRect(raw.canvasBounds)
|
|
167
|
+
};
|
|
161
168
|
}
|
|
162
169
|
function parseScrollPos(raw) {
|
|
163
170
|
if (!isRecord$1(raw)) return null;
|
|
@@ -653,6 +660,7 @@ function registerIpc(ctx) {
|
|
|
653
660
|
});
|
|
654
661
|
electron.ipcMain.handle(IPC.setViewport, (e, width, height, rawDsf) => {
|
|
655
662
|
assertRenderer(e);
|
|
663
|
+
viewportArrived = true;
|
|
656
664
|
const dsf = parseDeviceScaleFactor(rawDsf);
|
|
657
665
|
if (dsf === null) throw new Error("invalid deviceScaleFactor");
|
|
658
666
|
const v = target.setViewport(width, height, dsf);
|
|
@@ -707,6 +715,7 @@ function registerIpc(ctx) {
|
|
|
707
715
|
waiter(report);
|
|
708
716
|
});
|
|
709
717
|
const scrollBoth = async (req) => {
|
|
718
|
+
await awaitViewportStable();
|
|
710
719
|
const base = { x: req.x, y: req.y };
|
|
711
720
|
if (req.selector !== void 0) base.selector = req.selector;
|
|
712
721
|
if (!native.webContents.isDestroyed()) native.webContents.send(IPC.applyScroll, base);
|
|
@@ -759,6 +768,7 @@ function registerIpc(ctx) {
|
|
|
759
768
|
});
|
|
760
769
|
const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", mode: "url" };
|
|
761
770
|
let targetBounds = null;
|
|
771
|
+
let canvasBounds = null;
|
|
762
772
|
const MAX_PENDING_APPLIES = 32;
|
|
763
773
|
let rendererReported = false;
|
|
764
774
|
let warnedPendingOverflow = false;
|
|
@@ -767,9 +777,10 @@ function registerIpc(ctx) {
|
|
|
767
777
|
if (!fromRenderer(e)) return;
|
|
768
778
|
const s = parseUiState(raw);
|
|
769
779
|
if (!s) return;
|
|
770
|
-
const { targetBounds: bounds, ...state } = s;
|
|
780
|
+
const { targetBounds: bounds, canvasBounds: canvas, ...state } = s;
|
|
771
781
|
Object.assign(uiState, state);
|
|
772
782
|
targetBounds = bounds ?? null;
|
|
783
|
+
canvasBounds = canvas ?? null;
|
|
773
784
|
if (!rendererReported) {
|
|
774
785
|
rendererReported = true;
|
|
775
786
|
for (const patch of pendingApplies.splice(0)) {
|
|
@@ -777,6 +788,58 @@ function registerIpc(ctx) {
|
|
|
777
788
|
}
|
|
778
789
|
}
|
|
779
790
|
});
|
|
791
|
+
let viewportPending = false;
|
|
792
|
+
let viewportArrived = false;
|
|
793
|
+
const VIEWPORT_ARRIVAL_MS = 600;
|
|
794
|
+
const awaitViewportStable = async () => {
|
|
795
|
+
if (!viewportPending) return;
|
|
796
|
+
viewportPending = false;
|
|
797
|
+
const arrivalDeadline = Date.now() + VIEWPORT_ARRIVAL_MS;
|
|
798
|
+
while (!viewportArrived && Date.now() < arrivalDeadline) {
|
|
799
|
+
await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
|
|
800
|
+
}
|
|
801
|
+
await settleTarget();
|
|
802
|
+
};
|
|
803
|
+
const SETTLE_POLL_MS = 80;
|
|
804
|
+
const SETTLE_STABLE_READS = 2;
|
|
805
|
+
const SETTLE_BUDGET_MS = 4e3;
|
|
806
|
+
const SETTLE_DRAW_MS = 120;
|
|
807
|
+
const nextFrame = (budgetMs) => new Promise((resolve) => {
|
|
808
|
+
const timer = setTimeout(() => {
|
|
809
|
+
target.off("frame", onFrame);
|
|
810
|
+
resolve(false);
|
|
811
|
+
}, budgetMs);
|
|
812
|
+
const onFrame = () => {
|
|
813
|
+
clearTimeout(timer);
|
|
814
|
+
target.off("frame", onFrame);
|
|
815
|
+
resolve(true);
|
|
816
|
+
};
|
|
817
|
+
target.on("frame", onFrame);
|
|
818
|
+
target.invalidate();
|
|
819
|
+
});
|
|
820
|
+
const settleTarget = async () => {
|
|
821
|
+
const deadline = Date.now() + SETTLE_BUDGET_MS;
|
|
822
|
+
let last = "";
|
|
823
|
+
let stable = 0;
|
|
824
|
+
while (Date.now() < deadline) {
|
|
825
|
+
const v = target.getViewport();
|
|
826
|
+
const key = `${v.width}x${v.height}`;
|
|
827
|
+
stable = key === last ? stable + 1 : 0;
|
|
828
|
+
last = key;
|
|
829
|
+
if (stable >= SETTLE_STABLE_READS) break;
|
|
830
|
+
await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
|
|
831
|
+
}
|
|
832
|
+
if (stable < SETTLE_STABLE_READS) return false;
|
|
833
|
+
const painted = await nextFrame(Math.max(0, deadline - Date.now()));
|
|
834
|
+
await new Promise((r) => setTimeout(r, SETTLE_DRAW_MS));
|
|
835
|
+
return painted;
|
|
836
|
+
};
|
|
837
|
+
const roundRect = (r) => ({
|
|
838
|
+
x: Math.round(r.x),
|
|
839
|
+
y: Math.round(r.y),
|
|
840
|
+
width: Math.max(1, Math.round(r.width)),
|
|
841
|
+
height: Math.max(1, Math.round(r.height))
|
|
842
|
+
});
|
|
780
843
|
const appVersion = (() => {
|
|
781
844
|
try {
|
|
782
845
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(__dirname, "..", "..", "package.json"), "utf8"));
|
|
@@ -831,6 +894,10 @@ function registerIpc(ctx) {
|
|
|
831
894
|
navigate: navigateBoth,
|
|
832
895
|
apply: (patch) => {
|
|
833
896
|
if (win.isDestroyed()) return;
|
|
897
|
+
if (patch.presetId !== void 0) {
|
|
898
|
+
viewportPending = true;
|
|
899
|
+
viewportArrived = false;
|
|
900
|
+
}
|
|
834
901
|
if (!rendererReported) {
|
|
835
902
|
if (pendingApplies.length >= MAX_PENDING_APPLIES) {
|
|
836
903
|
if (!warnedPendingOverflow) {
|
|
@@ -845,20 +912,28 @@ function registerIpc(ctx) {
|
|
|
845
912
|
win.webContents.send(IPC.agentApply, patch);
|
|
846
913
|
},
|
|
847
914
|
captureVisible: async () => {
|
|
915
|
+
await awaitViewportStable();
|
|
916
|
+
await settleTarget();
|
|
848
917
|
const image = await win.webContents.capturePage();
|
|
849
918
|
const size = image.getSize();
|
|
850
919
|
return { data: image.toPNG().toString("base64"), width: size.width, height: size.height };
|
|
851
920
|
},
|
|
852
921
|
captureTarget: async () => {
|
|
853
|
-
|
|
922
|
+
await awaitViewportStable();
|
|
923
|
+
const settled = await settleTarget();
|
|
924
|
+
const bounds = canvasBounds ?? targetBounds;
|
|
854
925
|
const known = bounds !== null && bounds.width >= 1 && bounds.height >= 1;
|
|
855
|
-
const image = await win.webContents.capturePage(known ? bounds : void 0);
|
|
926
|
+
const image = await win.webContents.capturePage(known ? roundRect(bounds) : void 0);
|
|
856
927
|
const size = image.getSize();
|
|
928
|
+
const warnings = [];
|
|
929
|
+
if (!known) warnings.push("the renderer has not reported the pane bounds yet; captured the full window instead");
|
|
930
|
+
else if (canvasBounds === null) warnings.push("the renderer has not reported the render bounds yet; captured the whole pane instead");
|
|
931
|
+
if (!settled) warnings.push("the target was still resizing when the capture budget ran out; the PNG may show a transitional frame");
|
|
857
932
|
return {
|
|
858
933
|
data: image.toPNG().toString("base64"),
|
|
859
934
|
width: size.width,
|
|
860
935
|
height: size.height,
|
|
861
|
-
warnings
|
|
936
|
+
warnings
|
|
862
937
|
};
|
|
863
938
|
},
|
|
864
939
|
viewport: () => target.getViewport(),
|
package/out/mcp/server.js
CHANGED
|
@@ -128,8 +128,9 @@ const snapInputShape = {
|
|
|
128
128
|
capture: zod_1.z
|
|
129
129
|
.enum(['window', 'pane'])
|
|
130
130
|
.optional()
|
|
131
|
-
.describe("Live mode only: what the returned PNG shows — 'window' (default) is the whole app window, 'pane' is
|
|
132
|
-
'
|
|
131
|
+
.describe("Live mode only: what the returned PNG shows — 'window' (default) is the whole app window, 'pane' is the " +
|
|
132
|
+
'rendered screen cropped to itself, so a minified mobile preset is phone-shaped rather than a small phone ' +
|
|
133
|
+
'in a large rectangle. Ignored (with a note) when the render is headless.'),
|
|
133
134
|
};
|
|
134
135
|
const snapOutputShape = {
|
|
135
136
|
mode: zod_1.z
|
|
@@ -290,10 +291,11 @@ const driveInputShape = {
|
|
|
290
291
|
capture: zod_1.z
|
|
291
292
|
.enum(['window', 'pane'])
|
|
292
293
|
.optional()
|
|
293
|
-
.describe("Capture the app after the commands run: 'pane' crops to the
|
|
294
|
-
"
|
|
295
|
-
'
|
|
296
|
-
'
|
|
294
|
+
.describe("Capture the app after the commands run: 'pane' crops to the rendered screen itself (the render, not the " +
|
|
295
|
+
"empty pane around it — a minified mobile preset comes back phone-shaped), 'window' takes the whole app " +
|
|
296
|
+
'window. The capture waits for a preset resize to finish first, so the PNG matches the status beside it. ' +
|
|
297
|
+
'This is how you see a scrolled or panned state — unlike obsrv_snap, nothing is navigated, so the scroll ' +
|
|
298
|
+
'position survives. The PNG comes back inline when it is within the 1.5 MiB cap, and always as pngPath.'),
|
|
297
299
|
};
|
|
298
300
|
const driveOutputShape = {
|
|
299
301
|
version: zod_1.z.string().describe('The running app version.'),
|
|
@@ -14499,6 +14499,7 @@ function App() {
|
|
|
14499
14499
|
const dropToken = reactExports.useRef(0);
|
|
14500
14500
|
const targetPaneRef = reactExports.useRef(null);
|
|
14501
14501
|
const [targetBounds, setTargetBounds] = reactExports.useState(null);
|
|
14502
|
+
const [canvasBounds, setCanvasBounds] = reactExports.useState(null);
|
|
14502
14503
|
const toggle = (which) => () => setDrawer((d) => d === which ? "none" : which);
|
|
14503
14504
|
const setHost = useStore((s) => s.setHost);
|
|
14504
14505
|
const setSettings = useStore((s) => s.setSettings);
|
|
@@ -14547,17 +14548,34 @@ function App() {
|
|
|
14547
14548
|
const el = targetPaneRef.current;
|
|
14548
14549
|
if (!el) return;
|
|
14549
14550
|
const measure = () => {
|
|
14550
|
-
const
|
|
14551
|
-
setTargetBounds({ x:
|
|
14551
|
+
const pane = el.getBoundingClientRect();
|
|
14552
|
+
setTargetBounds({ x: pane.x, y: pane.y, width: pane.width, height: pane.height });
|
|
14553
|
+
const canvas2 = el.querySelector("canvas");
|
|
14554
|
+
if (!canvas2) {
|
|
14555
|
+
setCanvasBounds(null);
|
|
14556
|
+
return;
|
|
14557
|
+
}
|
|
14558
|
+
const c = canvas2.getBoundingClientRect();
|
|
14559
|
+
const x = Math.max(pane.x, c.x);
|
|
14560
|
+
const y = Math.max(pane.y, c.y);
|
|
14561
|
+
const right = Math.min(pane.x + pane.width, c.x + c.width);
|
|
14562
|
+
const bottom = Math.min(pane.y + pane.height, c.y + c.height);
|
|
14563
|
+
setCanvasBounds(right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null);
|
|
14552
14564
|
};
|
|
14553
14565
|
const ro = new ResizeObserver(measure);
|
|
14554
14566
|
ro.observe(el);
|
|
14567
|
+
const canvas = el.querySelector("canvas");
|
|
14568
|
+
if (canvas) ro.observe(canvas);
|
|
14569
|
+
el.addEventListener("scroll", measure, { passive: true });
|
|
14555
14570
|
measure();
|
|
14556
|
-
return () =>
|
|
14571
|
+
return () => {
|
|
14572
|
+
ro.disconnect();
|
|
14573
|
+
el.removeEventListener("scroll", measure);
|
|
14574
|
+
};
|
|
14557
14575
|
}, []);
|
|
14558
14576
|
reactExports.useEffect(() => {
|
|
14559
|
-
window.obsrv.reportUiState({ presetId, profileId, viewMode, mode, targetBounds });
|
|
14560
|
-
}, [presetId, profileId, viewMode, mode, targetBounds]);
|
|
14577
|
+
window.obsrv.reportUiState({ presetId, profileId, viewMode, mode, targetBounds, canvasBounds });
|
|
14578
|
+
}, [presetId, profileId, viewMode, mode, targetBounds, canvasBounds]);
|
|
14561
14579
|
reactExports.useEffect(() => {
|
|
14562
14580
|
return window.obsrv.onAgentApply((patch) => {
|
|
14563
14581
|
const s = useStore.getState();
|
package/out/renderer/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:" />
|
|
6
6
|
<title>Obsrv</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-Cgx8iixu.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="./assets/index-BeroS4wv.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -153,7 +153,14 @@ function parseUiState(raw) {
|
|
|
153
153
|
return null;
|
|
154
154
|
if (mode !== 'url' && mode !== 'image')
|
|
155
155
|
return null;
|
|
156
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
presetId,
|
|
158
|
+
profileId,
|
|
159
|
+
viewMode,
|
|
160
|
+
mode,
|
|
161
|
+
targetBounds: parseRect(raw.targetBounds),
|
|
162
|
+
canvasBounds: parseRect(raw.canvasBounds),
|
|
163
|
+
};
|
|
157
164
|
}
|
|
158
165
|
/**
|
|
159
166
|
* A scroll offset reported by the sync preload in a page webContents. Both
|