getobsrv 0.7.0 → 0.7.1

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;
@@ -759,6 +766,7 @@ function registerIpc(ctx) {
759
766
  });
760
767
  const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", mode: "url" };
761
768
  let targetBounds = null;
769
+ let canvasBounds = null;
762
770
  const MAX_PENDING_APPLIES = 32;
763
771
  let rendererReported = false;
764
772
  let warnedPendingOverflow = false;
@@ -767,9 +775,10 @@ function registerIpc(ctx) {
767
775
  if (!fromRenderer(e)) return;
768
776
  const s = parseUiState(raw);
769
777
  if (!s) return;
770
- const { targetBounds: bounds, ...state } = s;
778
+ const { targetBounds: bounds, canvasBounds: canvas, ...state } = s;
771
779
  Object.assign(uiState, state);
772
780
  targetBounds = bounds ?? null;
781
+ canvasBounds = canvas ?? null;
773
782
  if (!rendererReported) {
774
783
  rendererReported = true;
775
784
  for (const patch of pendingApplies.splice(0)) {
@@ -777,6 +786,46 @@ function registerIpc(ctx) {
777
786
  }
778
787
  }
779
788
  });
789
+ const SETTLE_POLL_MS = 80;
790
+ const SETTLE_STABLE_READS = 2;
791
+ const SETTLE_BUDGET_MS = 4e3;
792
+ const SETTLE_DRAW_MS = 120;
793
+ const nextFrame = (budgetMs) => new Promise((resolve) => {
794
+ const timer = setTimeout(() => {
795
+ target.off("frame", onFrame);
796
+ resolve(false);
797
+ }, budgetMs);
798
+ const onFrame = () => {
799
+ clearTimeout(timer);
800
+ target.off("frame", onFrame);
801
+ resolve(true);
802
+ };
803
+ target.on("frame", onFrame);
804
+ target.invalidate();
805
+ });
806
+ const settleTarget = async () => {
807
+ const deadline = Date.now() + SETTLE_BUDGET_MS;
808
+ let last = "";
809
+ let stable = 0;
810
+ while (Date.now() < deadline) {
811
+ const v = target.getViewport();
812
+ const key = `${v.width}x${v.height}`;
813
+ stable = key === last ? stable + 1 : 0;
814
+ last = key;
815
+ if (stable >= SETTLE_STABLE_READS) break;
816
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
817
+ }
818
+ if (stable < SETTLE_STABLE_READS) return false;
819
+ const painted = await nextFrame(Math.max(0, deadline - Date.now()));
820
+ await new Promise((r) => setTimeout(r, SETTLE_DRAW_MS));
821
+ return painted;
822
+ };
823
+ const roundRect = (r) => ({
824
+ x: Math.round(r.x),
825
+ y: Math.round(r.y),
826
+ width: Math.max(1, Math.round(r.width)),
827
+ height: Math.max(1, Math.round(r.height))
828
+ });
780
829
  const appVersion = (() => {
781
830
  try {
782
831
  const pkg = JSON.parse(node_fs.readFileSync(node_path.join(__dirname, "..", "..", "package.json"), "utf8"));
@@ -845,20 +894,26 @@ function registerIpc(ctx) {
845
894
  win.webContents.send(IPC.agentApply, patch);
846
895
  },
847
896
  captureVisible: async () => {
897
+ await settleTarget();
848
898
  const image = await win.webContents.capturePage();
849
899
  const size = image.getSize();
850
900
  return { data: image.toPNG().toString("base64"), width: size.width, height: size.height };
851
901
  },
852
902
  captureTarget: async () => {
853
- const bounds = targetBounds;
903
+ const settled = await settleTarget();
904
+ const bounds = canvasBounds ?? targetBounds;
854
905
  const known = bounds !== null && bounds.width >= 1 && bounds.height >= 1;
855
- const image = await win.webContents.capturePage(known ? bounds : void 0);
906
+ const image = await win.webContents.capturePage(known ? roundRect(bounds) : void 0);
856
907
  const size = image.getSize();
908
+ const warnings = [];
909
+ if (!known) warnings.push("the renderer has not reported the pane bounds yet; captured the full window instead");
910
+ else if (canvasBounds === null) warnings.push("the renderer has not reported the render bounds yet; captured the whole pane instead");
911
+ if (!settled) warnings.push("the target was still resizing when the capture budget ran out; the PNG may show a transitional frame");
857
912
  return {
858
913
  data: image.toPNG().toString("base64"),
859
914
  width: size.width,
860
915
  height: size.height,
861
- warnings: known ? [] : ["the renderer has not reported the target pane bounds yet; captured the full window instead"]
916
+ warnings
862
917
  };
863
918
  },
864
919
  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.1",
4
4
  "description": "See your site the way 1x screens see it",
5
5
  "main": "./out/main/index.js",
6
6
  "bin": {