getobsrv 0.8.0 → 0.10.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/README.md CHANGED
@@ -73,11 +73,22 @@ the advanced sliders. Enter your own monitor's diagonal in Settings once so the
73
73
  pane renders at true physical size. Drop a 2x/3x PNG or JPG export to check a design
74
74
  before it's built.
75
75
 
76
- The target pane opens at 1:1, which usually overflows the pane: pan with a
77
- middle-button drag, Option+drag or Option+wheel, or switch the toolbar's
78
- `1:1 / Fit` control to a fit-to-pane overview (smoothly minified, so not
79
- pixel-exact — the footer says so) and click anywhere in it to jump back to 1:1
80
- at that spot.
76
+ The target pane opens in **Fit**, a fit-to-pane view of the whole render. Fit
77
+ never enlarges past 1:1, so a render that already fits is shown at true size;
78
+ a larger one is smoothly minified to fit, which is not pixel-exact — the footer
79
+ says so, and names the magnification. Fit is fully interactive: clicks, drags,
80
+ the wheel and the keyboard all reach the page.
81
+
82
+ Switch the toolbar's `1:1 / Fit` control to **1:1** for true magnification,
83
+ which usually overflows the pane: pan with a middle-button drag, Option+drag or
84
+ Option+wheel. From fit, **Option+click** jumps straight to 1:1 with the clicked
85
+ spot centred (plain clicks belong to the page).
86
+
87
+ The `Both / Target` control beside it hides the native pane so the target render
88
+ takes the whole window — useful for a small mobile preset that would otherwise
89
+ sit in half a window, and for agent captures. The native pane stays loaded while
90
+ hidden, so the URL bar, back/forward and link clicks keep working exactly as
91
+ they do side by side.
81
92
 
82
93
  ## Agent & CI use
83
94
 
@@ -116,7 +127,9 @@ raster density — the PNG comes back as an inline image up to 1.5 MiB),
116
127
  If the desktop app is open with the toolbar's **Agent control** toggle on,
117
128
  `obsrv_snap` drives the *visible* window instead: you watch the URL load and
118
129
  the preset flip, and the agent gets back a capture of the app exactly as you
119
- see it (plus `obsrv_drive` to flip URL/preset/profile directly). Agents can
130
+ see it (plus `obsrv_drive` to flip URL/preset/profile/panes directly `panes:
131
+ 'target'` gives the target render the whole window, which is usually what you
132
+ want before a capture). Agents can
120
133
  also scroll, click, pan and highlight while you watch — a drive session works
121
134
  as a guided demo. A `scroll` reports the offset it actually reached
122
135
  (`scrolled` / `scroller`), finds the inner scroll container on pages whose
package/out/main/index.js CHANGED
@@ -14,6 +14,7 @@ const IPC = {
14
14
  forward: "obsrv:forward",
15
15
  setViewport: "obsrv:set-viewport",
16
16
  setNativeBounds: "obsrv:set-native-bounds",
17
+ setNativeVisible: "obsrv:set-native-visible",
17
18
  setMode: "obsrv:set-mode",
18
19
  sendInput: "obsrv:send-input",
19
20
  getHostInfo: "obsrv:get-host-info",
@@ -26,6 +27,7 @@ const IPC = {
26
27
  hostChanged: "obsrv:host-changed",
27
28
  targetLoading: "obsrv:target-loading",
28
29
  targetNavigating: "obsrv:target-navigating",
30
+ nativeFocused: "obsrv:native-focused",
29
31
  syncScroll: "obsrv:sync-scroll",
30
32
  applyScroll: "obsrv:apply-scroll",
31
33
  scrollResult: "obsrv:scroll-result",
@@ -157,10 +159,13 @@ function parseUiState(raw) {
157
159
  if (typeof profileId !== "string" || profileId.length === 0 || profileId.length > MAX_UI_ID) return null;
158
160
  if (viewMode !== "1:1" && viewMode !== "fit") return null;
159
161
  if (mode !== "url" && mode !== "image") return null;
162
+ const panes = raw.panes ?? "both";
163
+ if (panes !== "both" && panes !== "target") return null;
160
164
  return {
161
165
  presetId,
162
166
  profileId,
163
167
  viewMode,
168
+ panes,
164
169
  mode,
165
170
  targetBounds: parseRect(raw.targetBounds),
166
171
  canvasBounds: parseRect(raw.canvasBounds)
@@ -200,6 +205,7 @@ const CONTROL_COMMANDS = [
200
205
  "setPreset",
201
206
  "setProfile",
202
207
  "setViewMode",
208
+ "setPanes",
203
209
  "captureVisible",
204
210
  // v0.5 drive controls (spec §14 "Drive controls").
205
211
  "scroll",
@@ -244,6 +250,9 @@ function profileApplyError(id) {
244
250
  function viewModeApplyError(v) {
245
251
  return v === "1:1" || v === "fit" ? null : `setViewMode payload must be { mode: '1:1' | 'fit' }`;
246
252
  }
253
+ function panesApplyError(v) {
254
+ return v === "both" || v === "target" ? null : `setPanes payload must be { panes: 'both' | 'target' }`;
255
+ }
247
256
  function pixelExactApplyError(v) {
248
257
  return typeof v === "boolean" ? null : "setPixelExact payload must be { on: boolean }";
249
258
  }
@@ -466,6 +475,12 @@ class ControlServer {
466
475
  const mode = payload.mode;
467
476
  return this.applyAndConfirm({ viewMode: mode }, (s) => s.viewMode === mode);
468
477
  }
478
+ case "setPanes": {
479
+ const err = panesApplyError(payload.panes);
480
+ if (err) return reply(400, { error: err });
481
+ const panes = payload.panes;
482
+ return this.applyAndConfirm({ panes }, (s) => s.panes === panes);
483
+ }
469
484
  case "captureVisible": {
470
485
  const capture = await this.deps.captureVisible();
471
486
  return reply(200, { ok: true, ...capture });
@@ -603,7 +618,7 @@ async function checkForUpdate(current, now) {
603
618
  request.end();
604
619
  });
605
620
  }
606
- const TOOLBAR_H = 44;
621
+ const TOOLBAR_H = 82;
607
622
  const MAX_IMAGE_FILE_BYTES = 64 * 1024 * 1024;
608
623
  const SCROLL_REPLY_TIMEOUT_MS = 1e3;
609
624
  function hostInfo(win) {
@@ -675,13 +690,24 @@ function registerIpc(ctx) {
675
690
  } catch {
676
691
  }
677
692
  });
693
+ let modeIsLive = true;
694
+ let panesShowNative = true;
695
+ const applyNativeVisibility = () => {
696
+ native.setVisible(modeIsLive && panesShowNative);
697
+ };
678
698
  electron.ipcMain.on(IPC.setMode, (e, raw) => {
679
699
  if (!fromRenderer(e)) return;
680
700
  const mode = parseMode(raw);
681
701
  if (!mode) return;
682
- const live = mode === "url";
683
- native.setVisible(live);
684
- bus.setEnabled(live);
702
+ modeIsLive = mode === "url";
703
+ applyNativeVisibility();
704
+ bus.setEnabled(modeIsLive);
705
+ });
706
+ electron.ipcMain.on(IPC.setNativeVisible, (e, raw) => {
707
+ if (!fromRenderer(e)) return;
708
+ if (typeof raw !== "boolean") return;
709
+ panesShowNative = raw;
710
+ applyNativeVisibility();
685
711
  });
686
712
  let rendererDrivesLayout = false;
687
713
  const fallbackLayout = () => {
@@ -766,7 +792,7 @@ function registerIpc(ctx) {
766
792
  settings = s;
767
793
  if (s.agentControl !== wasEnabled) applyAgentControl(s.agentControl);
768
794
  });
769
- const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", mode: "url" };
795
+ const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "fit", panes: "both", mode: "url" };
770
796
  let targetBounds = null;
771
797
  let canvasBounds = null;
772
798
  const MAX_PENDING_APPLIES = 32;
@@ -1213,6 +1239,9 @@ function boot() {
1213
1239
  if (!win.isDestroyed()) win.webContents.send(IPC.openImagePath, path);
1214
1240
  }
1215
1241
  });
1242
+ native.webContents.on("focus", () => {
1243
+ if (!win.isDestroyed()) win.webContents.send(IPC.nativeFocused);
1244
+ });
1216
1245
  const target = new targetSource.TargetSource();
1217
1246
  target.on("load-error", (err) => {
1218
1247
  if (!win.isDestroyed()) win.webContents.send(IPC.loadError, err);
@@ -1227,7 +1256,7 @@ function boot() {
1227
1256
  const sync = attachSyncBus(native, target, (url) => {
1228
1257
  if (!win.isDestroyed()) win.webContents.send(IPC.urlChanged, url);
1229
1258
  });
1230
- const ctx = { win, native, target, bus, sync };
1259
+ const ctx = { win, native, target, bus, sync, toolbarH: TOOLBAR_H };
1231
1260
  registerIpc(ctx);
1232
1261
  installMenu(ctx);
1233
1262
  win.on("close", () => {
package/out/mcp/server.js CHANGED
@@ -160,6 +160,7 @@ const snapOutputShape = {
160
160
  presetId: zod_1.z.string().optional().describe('Live only: the screen preset selected in the app.'),
161
161
  profileId: zod_1.z.string().optional().describe('Live only: the panel profile selected in the app.'),
162
162
  viewMode: zod_1.z.string().optional().describe("Live only: the app's target-pane view (1:1 or fit)."),
163
+ panes: zod_1.z.string().optional().describe("Live only: 'both' (native pane beside the target) or 'target' (the target render has the whole window)."),
163
164
  width: zod_1.z
164
165
  .number()
165
166
  .optional()
@@ -245,6 +246,10 @@ const driveInputShape = {
245
246
  preset: zod_1.z.enum(PRESET_IDS).optional().describe('Apply this screen preset, exactly as clicking the toolbar would.'),
246
247
  profile: zod_1.z.enum(PROFILE_IDS).optional().describe('Apply this panel profile in the app.'),
247
248
  viewMode: zod_1.z.enum(['1:1', 'fit']).optional().describe("Switch the app's target pane between 1:1 (actual size) and fit."),
249
+ panes: zod_1.z
250
+ .enum(['both', 'target'])
251
+ .optional()
252
+ .describe("Show both panes, or give the target render the whole window ('target'). Solo target is usually what you want before a capture."),
248
253
  pixelExact: zod_1.z.boolean().optional().describe("Toggle the toolbar's pixel-exact checkbox (pins the magnification to the host scale)."),
249
254
  focus: zod_1.z.boolean().optional().describe('true: bring the Obsrv window to the front first, so the user sees what follows.'),
250
255
  reload: zod_1.z.boolean().optional().describe('true: reload both panes (the same action as the toolbar reload).'),
@@ -303,6 +308,7 @@ const driveOutputShape = {
303
308
  presetId: zod_1.z.string(),
304
309
  profileId: zod_1.z.string(),
305
310
  viewMode: zod_1.z.string(),
311
+ panes: zod_1.z.string(),
306
312
  mode: zod_1.z.string().describe("The app's pane mode: 'url' (live page) or 'image' (a dropped design export)."),
307
313
  scrolled: zod_1.z
308
314
  .object({ x: zod_1.z.number(), y: zod_1.z.number() })
@@ -475,6 +481,7 @@ async function liveSnap(app, input, notes) {
475
481
  presetId: status.presetId,
476
482
  profileId: status.profileId,
477
483
  viewMode: status.viewMode,
484
+ panes: status.panes,
478
485
  width,
479
486
  height,
480
487
  settled,
@@ -620,7 +627,7 @@ server.registerTool('obsrv_drive', {
620
627
  `both panes, pan the target pane to a pixel, click the live page, and highlight a rect with a temporary ` +
621
628
  `neutral marker, all while the user watches.\n\n` +
622
629
  `Only the supplied inputs run (none = just read the current state), in this fixed order: focus → url → ` +
623
- `preset → profile → viewMode → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
630
+ `preset → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
624
631
  `capture. ` +
625
632
  `The result is the final status: app version, the URL showing, and the selected preset/profile/view. A ` +
626
633
  `click that navigates is reflected in that status — the call waits briefly (up to 2 s) for the commit. A ` +
@@ -664,6 +671,9 @@ server.registerTool('obsrv_drive', {
664
671
  if (input.viewMode !== undefined) {
665
672
  await (0, control_2.controlCall)(live.info, 'setViewMode', { mode: input.viewMode }, LIVE_APPLY_TIMEOUT_MS);
666
673
  }
674
+ if (input.panes !== undefined) {
675
+ await (0, control_2.controlCall)(live.info, 'setPanes', { panes: input.panes }, LIVE_APPLY_TIMEOUT_MS);
676
+ }
667
677
  if (input.pixelExact !== undefined) {
668
678
  await (0, control_2.controlCall)(live.info, 'setPixelExact', { on: input.pixelExact }, LIVE_APPLY_TIMEOUT_MS);
669
679
  }
@@ -7,6 +7,7 @@ const IPC = {
7
7
  forward: "obsrv:forward",
8
8
  setViewport: "obsrv:set-viewport",
9
9
  setNativeBounds: "obsrv:set-native-bounds",
10
+ setNativeVisible: "obsrv:set-native-visible",
10
11
  setMode: "obsrv:set-mode",
11
12
  sendInput: "obsrv:send-input",
12
13
  getHostInfo: "obsrv:get-host-info",
@@ -19,6 +20,7 @@ const IPC = {
19
20
  hostChanged: "obsrv:host-changed",
20
21
  targetLoading: "obsrv:target-loading",
21
22
  targetNavigating: "obsrv:target-navigating",
23
+ nativeFocused: "obsrv:native-focused",
22
24
  openImage: "obsrv:open-image",
23
25
  focusUrl: "obsrv:focus-url",
24
26
  openImagePath: "obsrv:open-image-path",
@@ -57,6 +59,7 @@ const api = {
57
59
  forward: () => electron.ipcRenderer.send(IPC.forward),
58
60
  setViewport: (width, height, deviceScaleFactor) => electron.ipcRenderer.invoke(IPC.setViewport, width, height, deviceScaleFactor),
59
61
  setNativeBounds: (rect) => electron.ipcRenderer.send(IPC.setNativeBounds, rect),
62
+ setNativeVisible: (visible) => electron.ipcRenderer.send(IPC.setNativeVisible, visible),
60
63
  setMode: (mode) => electron.ipcRenderer.send(IPC.setMode, mode),
61
64
  sendInput: (ev) => electron.ipcRenderer.send(IPC.sendInput, ev),
62
65
  getHostInfo: () => electron.ipcRenderer.invoke(IPC.getHostInfo),
@@ -67,6 +70,13 @@ const api = {
67
70
  onLoadError: (cb) => subscribe(IPC.loadError, cb),
68
71
  onHostChanged: (cb) => subscribe(IPC.hostChanged, cb),
69
72
  onTargetLoading: (cb) => subscribe(IPC.targetLoading, cb),
73
+ onNativeFocused: (cb) => {
74
+ const listener = () => cb();
75
+ electron.ipcRenderer.on(IPC.nativeFocused, listener);
76
+ return () => {
77
+ electron.ipcRenderer.removeListener(IPC.nativeFocused, listener);
78
+ };
79
+ },
70
80
  onTargetNavigating: (cb) => {
71
81
  const listener = () => cb();
72
82
  electron.ipcRenderer.on(IPC.targetNavigating, listener);