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 +19 -6
- package/out/main/index.js +35 -6
- package/out/mcp/server.js +11 -1
- package/out/preload/app.js +10 -0
- package/out/renderer/assets/{index-Cgx8iixu.js → index-CQVE5hF3.js} +541 -153
- package/out/renderer/assets/{index-BeroS4wv.css → index-CZUlXhEb.css} +233 -82
- package/out/renderer/index.html +2 -2
- package/out/shared/control.js +12 -1
- package/out/shared/ipcPayloads.js +7 -0
- package/package.json +2 -1
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
bus.setEnabled(
|
|
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: "
|
|
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
|
}
|
package/out/preload/app.js
CHANGED
|
@@ -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);
|