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 { presetId, profileId, viewMode, mode, targetBounds: parseRect(raw.targetBounds) };
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
- const bounds = targetBounds;
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: known ? [] : ["the renderer has not reported the target pane bounds yet; captured the full window instead"]
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 just " +
132
- 'the target pane (its footer readout included). Ignored (with a note) when the render is headless.'),
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 target pane (the 1x render on its own), " +
294
- "'window' takes the whole app window. This is how you see a scrolled or panned state unlike obsrv_snap, " +
295
- 'nothing is navigated, so the scroll position survives. The PNG comes back inline when it is within the ' +
296
- '1.5 MiB cap, and always as pngPath.'),
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 r = el.getBoundingClientRect();
14551
- setTargetBounds({ x: r.x, y: r.y, width: r.width, height: r.height });
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 () => ro.disconnect();
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();
@@ -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-BP1S2N6S.js"></script>
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 { presetId, profileId, viewMode, mode, targetBounds: parseRect(raw.targetBounds) };
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getobsrv",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "See your site the way 1x screens see it",
5
5
  "main": "./out/main/index.js",
6
6
  "bin": {