getobsrv 0.11.0 → 0.13.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 +34 -0
- package/out/cli/args.js +52 -5
- package/out/main/cli.js +43 -9
- package/out/main/index.js +792 -202
- package/out/main/{targetSource-nbfc-Fe6.js → targetSource-DuRYHUo8.js} +53 -1
- package/out/mcp/lib.js +14 -1
- package/out/mcp/server.js +88 -9
- package/out/preload/app.js +19 -16
- package/out/renderer/assets/{index-BlbBg6lm.js → index-BHt9Vm0J.js} +470 -120
- package/out/renderer/assets/{index-B61fbgqt.css → index-BRkFfzcI.css} +142 -4
- package/out/renderer/index.html +2 -2
- package/out/shared/calibration.js +35 -0
- package/out/shared/control.js +71 -1
- package/out/shared/ipcPayloads.js +44 -1
- package/out/shared/presets.js +16 -1
- package/out/shared/tabList.js +46 -0
- package/package.json +1 -1
- package/skills/obsrv-screens/SKILL.md +7 -0
|
@@ -5,6 +5,8 @@ const node_path = require("node:path");
|
|
|
5
5
|
const MAX_VIEWPORT = 4096;
|
|
6
6
|
const SPLIT_MIN = 0.1;
|
|
7
7
|
const SPLIT_MAX = 0.9;
|
|
8
|
+
const MAX_TABS_MIN = 2;
|
|
9
|
+
const MAX_TABS_MAX = 32;
|
|
8
10
|
const DEFAULT_SETTINGS = {
|
|
9
11
|
hostDiagonalInches: 27,
|
|
10
12
|
hostNits: 500,
|
|
@@ -12,8 +14,13 @@ const DEFAULT_SETTINGS = {
|
|
|
12
14
|
updateCheck: true,
|
|
13
15
|
lastUpdateCheck: 0,
|
|
14
16
|
recordHistory: true,
|
|
15
|
-
split: 0.5
|
|
17
|
+
split: 0.5,
|
|
18
|
+
maxTabs: 12
|
|
16
19
|
};
|
|
20
|
+
const DEFAULT_ORIENTATION = "portrait";
|
|
21
|
+
function isOrientation(v) {
|
|
22
|
+
return v === "portrait" || v === "landscape";
|
|
23
|
+
}
|
|
17
24
|
const SCREEN_PRESETS = [
|
|
18
25
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
19
26
|
{ id: "laptop-768", label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: "laptop" },
|
|
@@ -52,6 +59,9 @@ function findProfile(id) {
|
|
|
52
59
|
if (!p) throw new Error(`unknown profile: ${id}`);
|
|
53
60
|
return p;
|
|
54
61
|
}
|
|
62
|
+
function screenShape(width, height) {
|
|
63
|
+
return width > height ? "landscape" : "portrait";
|
|
64
|
+
}
|
|
55
65
|
function maxCssViewport(deviceScaleFactor) {
|
|
56
66
|
const dsf = Number.isFinite(deviceScaleFactor) && deviceScaleFactor > 1 ? deviceScaleFactor : 1;
|
|
57
67
|
return Math.max(1, Math.floor(MAX_VIEWPORT / dsf));
|
|
@@ -144,6 +154,12 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
144
154
|
*/
|
|
145
155
|
internal = false;
|
|
146
156
|
disposed = false;
|
|
157
|
+
/**
|
|
158
|
+
* What the owner asked for, not what the current window happens to be doing.
|
|
159
|
+
* See `setPainting` — `recreate()` swaps in a fresh webContents that starts
|
|
160
|
+
* painting, so the wish has to outlive the window that was serving it.
|
|
161
|
+
*/
|
|
162
|
+
paintingWanted = true;
|
|
147
163
|
constructor(fps = DEFAULT_FPS, options = {}) {
|
|
148
164
|
super();
|
|
149
165
|
this.fps = fps;
|
|
@@ -171,6 +187,7 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
171
187
|
this.firstNavDone = false;
|
|
172
188
|
const wc = win.webContents;
|
|
173
189
|
wc.setFrameRate(this.fps);
|
|
190
|
+
if (!this.paintingWanted) wc.stopPainting();
|
|
174
191
|
wc.setAudioMuted(true);
|
|
175
192
|
this.defaultUserAgent ??= wc.getUserAgent();
|
|
176
193
|
wc.setUserAgent(this.dsf > 1 && this.mobileEmulation ? MOBILE_USER_AGENT : this.defaultUserAgent);
|
|
@@ -245,6 +262,16 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
245
262
|
if (this.win === win) this.firstNavDone = true;
|
|
246
263
|
});
|
|
247
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolves once this window's own initial `about:blank` has committed.
|
|
267
|
+
* A navigation issued before that lands is undone by it — the commit
|
|
268
|
+
* arrives late and `SyncBus` mirrors it into the native pane — so a caller
|
|
269
|
+
* that builds a source and immediately drives it waits here first. Follows
|
|
270
|
+
* the current window: a dsf change swaps in a fresh one with a fresh gate.
|
|
271
|
+
*/
|
|
272
|
+
get ready() {
|
|
273
|
+
return this.firstNavigation;
|
|
274
|
+
}
|
|
248
275
|
/**
|
|
249
276
|
* Mobile viewport semantics for dsf > 1 (see class doc). Post-commit only:
|
|
250
277
|
* enabling emulation before a window's first navigation commits segfaults
|
|
@@ -344,6 +371,26 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
344
371
|
getDeviceScaleFactor() {
|
|
345
372
|
return this.dsf;
|
|
346
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Stops or resumes rasterisation without touching the page. Offscreen
|
|
376
|
+
* rendering runs at a fixed frame rate with `backgroundThrottling: false`,
|
|
377
|
+
* so a source nobody is looking at would otherwise paint a full viewport
|
|
378
|
+
* forever for nobody. The page keeps its DOM, timers, network and scroll —
|
|
379
|
+
* only pixel production stops.
|
|
380
|
+
*/
|
|
381
|
+
setPainting(painting) {
|
|
382
|
+
if (this.paintingWanted === painting) return;
|
|
383
|
+
this.paintingWanted = painting;
|
|
384
|
+
if (this.win.isDestroyed()) return;
|
|
385
|
+
const wc = this.win.webContents;
|
|
386
|
+
if (wc.isDestroyed()) return;
|
|
387
|
+
if (painting) wc.startPainting();
|
|
388
|
+
else wc.stopPainting();
|
|
389
|
+
}
|
|
390
|
+
/** What was last asked of `setPainting`, not what the window is doing. */
|
|
391
|
+
get painting() {
|
|
392
|
+
return this.paintingWanted;
|
|
393
|
+
}
|
|
347
394
|
/** Forces a full-frame repaint, e.g. after the renderer loses its texture. */
|
|
348
395
|
invalidate() {
|
|
349
396
|
if (!this.win.isDestroyed()) this.win.webContents.invalidate();
|
|
@@ -370,8 +417,11 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
370
417
|
}
|
|
371
418
|
}
|
|
372
419
|
exports.ALLOWED_URL_SCHEMES = ALLOWED_URL_SCHEMES;
|
|
420
|
+
exports.DEFAULT_ORIENTATION = DEFAULT_ORIENTATION;
|
|
373
421
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
374
422
|
exports.IMAGE_EXTENSIONS = IMAGE_EXTENSIONS;
|
|
423
|
+
exports.MAX_TABS_MAX = MAX_TABS_MAX;
|
|
424
|
+
exports.MAX_TABS_MIN = MAX_TABS_MIN;
|
|
375
425
|
exports.PANEL_PROFILES = PANEL_PROFILES;
|
|
376
426
|
exports.SCREEN_PRESETS = SCREEN_PRESETS;
|
|
377
427
|
exports.SPLIT_MAX = SPLIT_MAX;
|
|
@@ -380,6 +430,8 @@ exports.TargetSource = TargetSource;
|
|
|
380
430
|
exports.classifyFileNavigation = classifyFileNavigation;
|
|
381
431
|
exports.findPreset = findPreset;
|
|
382
432
|
exports.findProfile = findProfile;
|
|
433
|
+
exports.isOrientation = isOrientation;
|
|
383
434
|
exports.maxCssViewport = maxCssViewport;
|
|
384
435
|
exports.normalizeUrl = normalizeUrl;
|
|
436
|
+
exports.screenShape = screenShape;
|
|
385
437
|
exports.urlSchemeError = urlSchemeError;
|
package/out/mcp/lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PANE_CAPTURE_HEADLESS_NOTE = exports.APP_NOT_REACHABLE = exports.urlSchemeError = exports.ALLOWED_URL_SCHEMES = exports.STDERR_TAIL_CHARS = exports.MAX_INLINE_IMAGE_BYTES = exports.UsageError = void 0;
|
|
3
|
+
exports.ORIENTATION_NOTE = exports.PANE_CAPTURE_HEADLESS_NOTE = exports.APP_NOT_REACHABLE = exports.urlSchemeError = exports.ALLOWED_URL_SCHEMES = exports.STDERR_TAIL_CHARS = exports.MAX_INLINE_IMAGE_BYTES = exports.UsageError = void 0;
|
|
4
4
|
exports.buildSnapArgs = buildSnapArgs;
|
|
5
5
|
exports.buildDiffArgs = buildDiffArgs;
|
|
6
6
|
exports.shouldInlineImage = shouldInlineImage;
|
|
@@ -46,6 +46,8 @@ function buildSnapArgs(input, outPath) {
|
|
|
46
46
|
const args = ['snap', input.url];
|
|
47
47
|
if (input.preset !== undefined)
|
|
48
48
|
args.push('--preset', input.preset);
|
|
49
|
+
if (input.orientation !== undefined)
|
|
50
|
+
args.push('--orientation', input.orientation);
|
|
49
51
|
if (custom) {
|
|
50
52
|
args.push('--width', String(input.width), '--height', String(input.height));
|
|
51
53
|
if (input.deviceScaleFactor !== undefined)
|
|
@@ -170,9 +172,20 @@ function extractTrailingJson(stdout) {
|
|
|
170
172
|
}
|
|
171
173
|
return null;
|
|
172
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* What `obsrv_presets` says about rotation. Stated once here rather than
|
|
177
|
+
* repeated per entry: it is true of every preset in the table, and a field
|
|
178
|
+
* saying "rotatable: true" fourteen times would carry less than one sentence.
|
|
179
|
+
*/
|
|
180
|
+
exports.ORIENTATION_NOTE = 'cssWidth/cssHeight are each preset\'s natural orientation — portrait for every mobile preset, ' +
|
|
181
|
+
'landscape for the monitor and laptop ones. Every preset rotates: pass orientation: "landscape" ' +
|
|
182
|
+
'to obsrv_snap or obsrv_drive to swap the two axes a quarter turn. Rotation changes nothing else — ' +
|
|
183
|
+
'the diagonal, deviceScaleFactor, ppi and physical size are all orientation-independent, so a ' +
|
|
184
|
+
'rotated screen is the same panel turned sideways rather than a different one.';
|
|
173
185
|
/** The `obsrv_presets` payload, straight from src/shared/presets.ts — no spawn. */
|
|
174
186
|
function listCatalog() {
|
|
175
187
|
return {
|
|
188
|
+
orientation: exports.ORIENTATION_NOTE,
|
|
176
189
|
presets: presets_1.SCREEN_PRESETS.map(p => ({
|
|
177
190
|
id: p.id,
|
|
178
191
|
label: p.label,
|
package/out/mcp/server.js
CHANGED
|
@@ -97,12 +97,21 @@ const profileField = zod_1.z
|
|
|
97
97
|
.enum(PROFILE_IDS)
|
|
98
98
|
.optional()
|
|
99
99
|
.describe('Panel simulation (contrast floor, gamut, bit depth, brightness). Default: reference (off).');
|
|
100
|
+
const orientationField = zod_1.z
|
|
101
|
+
.enum(['portrait', 'landscape'])
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Rotate the screen a quarter turn (default portrait). Presets store their natural orientation — ' +
|
|
104
|
+
'portrait for every mobile preset, landscape for the monitors and laptops — and this swaps the CSS ' +
|
|
105
|
+
"viewport's two axes on top of that. Nothing else changes: the diagonal, raster density and physical " +
|
|
106
|
+
'size are orientation-independent, so it is the same panel turned sideways. Use it to check a ' +
|
|
107
|
+
'landscape phone layout, or a monitor stood on end.');
|
|
100
108
|
const snapInputShape = {
|
|
101
109
|
url: urlField,
|
|
102
110
|
preset: zod_1.z
|
|
103
111
|
.enum(PRESET_IDS)
|
|
104
112
|
.optional()
|
|
105
113
|
.describe('Screen preset id (list them with obsrv_presets). Mutually exclusive with width/height. Default: 1080p-24.'),
|
|
114
|
+
orientation: orientationField,
|
|
106
115
|
width: zod_1.z.number().int().min(1).optional().describe('Custom CSS viewport width in px. Needs height; mutually exclusive with preset.'),
|
|
107
116
|
height: zod_1.z.number().int().min(1).optional().describe('Custom CSS viewport height in px. Needs width.'),
|
|
108
117
|
deviceScaleFactor: zod_1.z
|
|
@@ -138,8 +147,24 @@ const snapOutputShape = {
|
|
|
138
147
|
.describe('How the snap was produced: a headless render, or a capture of the visible Obsrv app window (live drive).'),
|
|
139
148
|
out: zod_1.z.string().optional().describe('Headless only: PNG path the CLI wrote (same file as pngPath).'),
|
|
140
149
|
preset: zod_1.z.string().optional().describe('Headless only: preset id, or "custom" for width/height runs.'),
|
|
141
|
-
cssWidth: zod_1.z
|
|
142
|
-
|
|
150
|
+
cssWidth: zod_1.z
|
|
151
|
+
.number()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Applied CSS viewport width, already rotated. Headless: grown under fullPage. Live: what the app is rendering.'),
|
|
154
|
+
cssHeight: zod_1.z
|
|
155
|
+
.number()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Applied CSS viewport height, already rotated. Headless: grown under fullPage. Live: what the app is rendering.'),
|
|
158
|
+
orientation: zod_1.z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("Live only: the app's rotation flag — 'portrait' (the preset as its table stores it) or 'landscape' " +
|
|
162
|
+
'(rotated a quarter turn). See `screenShape` for the shape that produced. Headless runs report the ' +
|
|
163
|
+
'applied `cssWidth`/`cssHeight` instead, which say the same thing exactly.'),
|
|
164
|
+
screenShape: zod_1.z.string().optional().describe('Live only. ' + "The shape the screen actually has: 'portrait' or 'landscape'. Derived from the CSS dimensions, not from " +
|
|
165
|
+
"the `orientation` flag beside it — the flag means 'the preset as its table stores it' vs 'rotated a " +
|
|
166
|
+
"quarter turn', so for a landscape-natural monitor preset the two diverge (a fresh 1080p-24 tab is " +
|
|
167
|
+
"orientation 'portrait' on a 1920x1080 landscape screen). Report this word to the user, not the flag."),
|
|
143
168
|
deviceScaleFactor: zod_1.z.number().optional().describe('Headless only.'),
|
|
144
169
|
profile: zod_1.z.string().optional().describe('Headless only: applied panel profile id.'),
|
|
145
170
|
settled: zod_1.z
|
|
@@ -161,6 +186,8 @@ const snapOutputShape = {
|
|
|
161
186
|
profileId: zod_1.z.string().optional().describe('Live only: the panel profile selected in the app.'),
|
|
162
187
|
viewMode: zod_1.z.string().optional().describe("Live only: the app's target-pane view (1:1 or fit)."),
|
|
163
188
|
panes: zod_1.z.string().optional().describe("Live only: 'both' (native pane beside the target) or 'target' (the target render has the whole window)."),
|
|
189
|
+
tabId: zod_1.z.string().optional().describe('Live only: which of the app\'s tabs was captured (the active one). Empty from an app older than tabs.'),
|
|
190
|
+
tabIndex: zod_1.z.number().optional().describe("Live only: that tab's 0-based position in the strip."),
|
|
164
191
|
width: zod_1.z
|
|
165
192
|
.number()
|
|
166
193
|
.optional()
|
|
@@ -216,6 +243,9 @@ const diffOutputShape = {
|
|
|
216
243
|
findings: zod_1.z.array(zod_1.z.string()).describe('Humanised per-band findings. Informational — thresholds are the caller\'s job.'),
|
|
217
244
|
};
|
|
218
245
|
const presetsOutputShape = {
|
|
246
|
+
orientation: zod_1.z
|
|
247
|
+
.string()
|
|
248
|
+
.describe('How the cssWidth/cssHeight below relate to rotation, and how to ask for the other orientation.'),
|
|
219
249
|
presets: zod_1.z.array(zod_1.z.object({
|
|
220
250
|
id: zod_1.z.string(),
|
|
221
251
|
label: zod_1.z.string(),
|
|
@@ -244,6 +274,7 @@ const driveInputShape = {
|
|
|
244
274
|
.optional()
|
|
245
275
|
.describe('Navigate the app (both panes) to this http://, https:// or file:// URL (bare hosts also work).'),
|
|
246
276
|
preset: zod_1.z.enum(PRESET_IDS).optional().describe('Apply this screen preset, exactly as clicking the toolbar would.'),
|
|
277
|
+
orientation: orientationField,
|
|
247
278
|
profile: zod_1.z.enum(PROFILE_IDS).optional().describe('Apply this panel profile in the app.'),
|
|
248
279
|
viewMode: zod_1.z.enum(['1:1', 'fit']).optional().describe("Switch the app's target pane between 1:1 (actual size) and fit."),
|
|
249
280
|
panes: zod_1.z
|
|
@@ -307,9 +338,28 @@ const driveOutputShape = {
|
|
|
307
338
|
url: zod_1.z.string().describe('The URL the target pane reports showing.'),
|
|
308
339
|
presetId: zod_1.z.string(),
|
|
309
340
|
profileId: zod_1.z.string(),
|
|
341
|
+
orientation: zod_1.z
|
|
342
|
+
.string()
|
|
343
|
+
.describe("The rotation flag: 'portrait' (the preset as its table stores it) or 'landscape' (rotated a quarter " +
|
|
344
|
+
"turn). This is what to pass back to change it — for the shape the screen actually has, read " +
|
|
345
|
+
'`screenShape`. Reported as \'portrait\' by an app older than rotation, which is what such an app shows.'),
|
|
346
|
+
screenShape: zod_1.z.string().describe("The shape the screen actually has: 'portrait' or 'landscape'. Derived from the CSS dimensions, not from " +
|
|
347
|
+
"the `orientation` flag beside it — the flag means 'the preset as its table stores it' vs 'rotated a " +
|
|
348
|
+
"quarter turn', so for a landscape-natural monitor preset the two diverge (a fresh 1080p-24 tab is " +
|
|
349
|
+
"orientation 'portrait' on a 1920x1080 landscape screen). Report this word to the user, not the flag."),
|
|
350
|
+
cssWidth: zod_1.z
|
|
351
|
+
.number()
|
|
352
|
+
.describe('The CSS viewport the target is rendering at, already rotated. 0 from an app that predates the field.'),
|
|
353
|
+
cssHeight: zod_1.z.number().describe('The CSS viewport height, already rotated. 0 from an app that predates the field.'),
|
|
310
354
|
viewMode: zod_1.z.string(),
|
|
311
355
|
panes: zod_1.z.string(),
|
|
312
356
|
mode: zod_1.z.string().describe("The app's pane mode: 'url' (live page) or 'image' (a dropped design export)."),
|
|
357
|
+
tabId: zod_1.z
|
|
358
|
+
.string()
|
|
359
|
+
.describe('Which of the app\'s tabs this acted on. Every command resolves the active tab as it arrives, so a tabId ' +
|
|
360
|
+
'that changed between two calls means the user switched tabs under you. Empty string from an app older ' +
|
|
361
|
+
'than tabs, which has only one.'),
|
|
362
|
+
tabIndex: zod_1.z.number().describe('That tab\'s 0-based position in the strip.'),
|
|
313
363
|
scrolled: zod_1.z
|
|
314
364
|
.object({ x: zod_1.z.number(), y: zod_1.z.number() })
|
|
315
365
|
.nullable()
|
|
@@ -434,6 +484,9 @@ async function liveSnap(app, input, notes) {
|
|
|
434
484
|
}
|
|
435
485
|
if (input.preset !== undefined)
|
|
436
486
|
await (0, control_2.controlCall)(info, 'setPreset', { id: input.preset }, LIVE_APPLY_TIMEOUT_MS);
|
|
487
|
+
if (input.orientation !== undefined) {
|
|
488
|
+
await (0, control_2.controlCall)(info, 'setOrientation', { orientation: input.orientation }, LIVE_APPLY_TIMEOUT_MS);
|
|
489
|
+
}
|
|
437
490
|
if (input.profile !== undefined)
|
|
438
491
|
await (0, control_2.controlCall)(info, 'setProfile', { id: input.profile }, LIVE_APPLY_TIMEOUT_MS);
|
|
439
492
|
}
|
|
@@ -480,8 +533,14 @@ async function liveSnap(app, input, notes) {
|
|
|
480
533
|
url: status.url,
|
|
481
534
|
presetId: status.presetId,
|
|
482
535
|
profileId: status.profileId,
|
|
536
|
+
orientation: status.orientation,
|
|
537
|
+
screenShape: status.screenShape,
|
|
538
|
+
cssWidth: status.cssWidth,
|
|
539
|
+
cssHeight: status.cssHeight,
|
|
483
540
|
viewMode: status.viewMode,
|
|
484
541
|
panes: status.panes,
|
|
542
|
+
tabId: status.tabId,
|
|
543
|
+
tabIndex: status.tabIndex,
|
|
485
544
|
width,
|
|
486
545
|
height,
|
|
487
546
|
settled,
|
|
@@ -505,7 +564,8 @@ server.registerTool('obsrv_snap', {
|
|
|
505
564
|
`presets, the device's 2x/3x DPR plus mobile UA and viewport semantics for phone presets — optionally ` +
|
|
506
565
|
`through a cheap-panel simulation, and return the PNG. Use it to judge how a page actually looks on the ` +
|
|
507
566
|
`screens users own (1366×768 laptops, 1080p desktops, budget Androids) before declaring frontend work done.\n\n` +
|
|
508
|
-
`Pass either \`preset\` (list ids with obsrv_presets) or custom \`width\` + \`height\`, never both
|
|
567
|
+
`Pass either \`preset\` (list ids with obsrv_presets) or custom \`width\` + \`height\`, never both; either can be ` +
|
|
568
|
+
`rotated with \`orientation: "landscape"\`, which is how you check a phone's landscape layout. ` +
|
|
509
569
|
`Returns structured metadata (applied viewport, profile, \`settled\`, warnings, and \`pngPath\` — the PNG ` +
|
|
510
570
|
`kept in a per-call temp dir) plus the PNG as an inline image when it is within the 1.5 MiB cap; larger ` +
|
|
511
571
|
`captures (typically fullPage) stay on disk with a note.\n\n` +
|
|
@@ -520,7 +580,11 @@ server.registerTool('obsrv_snap', {
|
|
|
520
580
|
`capture; that visible steering is the point of live mode.\n\n` +
|
|
521
581
|
`A live snap only navigates when the app is showing a different URL; the result's \`navigated\` says which ` +
|
|
522
582
|
`happened. Navigating is a fresh load, so it starts at the top of the page — to photograph a scrolled or ` +
|
|
523
|
-
`panned state, use obsrv_drive with \`capture\` instead, which never navigates unless you ask it to
|
|
583
|
+
`panned state, use obsrv_drive with \`capture\` instead, which never navigates unless you ask it to.\n\n` +
|
|
584
|
+
`Tabs: the app can hold several sessions open as tabs, and a live snap acts on the one in front — which the ` +
|
|
585
|
+
`user can change at any moment. The result names it (\`tabId\`, \`tabIndex\`); compare across calls if you ` +
|
|
586
|
+
`need to know it did not move. There is no way to name a different tab, and no way to open, close or ` +
|
|
587
|
+
`switch tabs — those are the user's.`,
|
|
524
588
|
inputSchema: snapInputShape,
|
|
525
589
|
outputSchema: snapOutputShape,
|
|
526
590
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
@@ -621,15 +685,16 @@ server.registerTool('obsrv_diff', {
|
|
|
621
685
|
});
|
|
622
686
|
server.registerTool('obsrv_drive', {
|
|
623
687
|
title: 'Drive the visible Obsrv app',
|
|
624
|
-
description: `Drive the Obsrv desktop app the user is looking at: navigate it to a URL, apply a screen preset,
|
|
625
|
-
`profile, the target pane's 1:1/fit view or pixel-exact
|
|
688
|
+
description: `Drive the Obsrv desktop app the user is looking at: navigate it to a URL, apply a screen preset, rotate that ` +
|
|
689
|
+
`screen to landscape or portrait, apply a panel profile, the target pane's 1:1/fit view or pixel-exact ` +
|
|
690
|
+
`toggle — each exactly as clicking the toolbar would ` +
|
|
626
691
|
`— and steer the session like a guided demo: focus the window, step history (back/forward/reload), scroll ` +
|
|
627
692
|
`both panes, pan the target pane to a pixel, click the live page, and highlight a rect with a temporary ` +
|
|
628
693
|
`neutral marker, all while the user watches.\n\n` +
|
|
629
694
|
`Only the supplied inputs run (none = just read the current state), in this fixed order: focus → url → ` +
|
|
630
|
-
`preset → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
695
|
+
`preset → orientation → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
631
696
|
`capture. ` +
|
|
632
|
-
`The result is the final status: app version, the URL showing, and the selected preset/profile/view. A ` +
|
|
697
|
+
`The result is the final status: app version, the URL showing, and the selected preset/orientation/profile/view. A ` +
|
|
633
698
|
`click that navigates is reflected in that status — the call waits briefly (up to 2 s) for the commit. A ` +
|
|
634
699
|
`scroll adds \`scrolled\` (the offset actually reached) and \`scroller\` ('root' or 'element'): compare ` +
|
|
635
700
|
`\`scrolled\` with what you asked for rather than trusting the call's success, and use \`scroll.scrollSelector\` ` +
|
|
@@ -641,6 +706,12 @@ server.registerTool('obsrv_drive', {
|
|
|
641
706
|
`pass \`url\`, so this is how you photograph a scrolled or panned state: scroll, then capture, in one call. ` +
|
|
642
707
|
`obsrv_snap is the other way round — it points the app at a URL first, and pointing it somewhere new is a ` +
|
|
643
708
|
`fresh load that starts at the top.\n\n` +
|
|
709
|
+
`Tabs: the app can hold several sessions open as tabs, each with its own URL, screen preset and page state. ` +
|
|
710
|
+
`Every command here acts on whichever tab is in front *when that command arrives* — nothing is bound to a ` +
|
|
711
|
+
`tab for the length of the call — and the returned status names it (\`tabId\`, \`tabIndex\`). A \`tabId\` ` +
|
|
712
|
+
`that changed between two calls means the user switched tabs under you; re-read the state before trusting ` +
|
|
713
|
+
`what you knew. You cannot name a different tab, nor open, close or switch tabs — those are the user's. An ` +
|
|
714
|
+
`empty \`tabId\` means an app older than tabs, which has only the one.\n\n` +
|
|
644
715
|
`Requires the app to be open with its "Agent control" toolbar toggle on; errors otherwise. This tool ` +
|
|
645
716
|
`mutates visible app state (it changes what the user's window shows, and a click can act on the live page).`,
|
|
646
717
|
inputSchema: driveInputShape,
|
|
@@ -666,6 +737,12 @@ server.registerTool('obsrv_drive', {
|
|
|
666
737
|
}
|
|
667
738
|
if (input.preset !== undefined)
|
|
668
739
|
await (0, control_2.controlCall)(live.info, 'setPreset', { id: input.preset }, LIVE_APPLY_TIMEOUT_MS);
|
|
740
|
+
// After the preset, before everything else: rotation is applied on top of
|
|
741
|
+
// whichever screen is in force, so a call carrying both has to land in
|
|
742
|
+
// that order or the rotation would be spent on the outgoing preset.
|
|
743
|
+
if (input.orientation !== undefined) {
|
|
744
|
+
await (0, control_2.controlCall)(live.info, 'setOrientation', { orientation: input.orientation }, LIVE_APPLY_TIMEOUT_MS);
|
|
745
|
+
}
|
|
669
746
|
if (input.profile !== undefined)
|
|
670
747
|
await (0, control_2.controlCall)(live.info, 'setProfile', { id: input.profile }, LIVE_APPLY_TIMEOUT_MS);
|
|
671
748
|
if (input.viewMode !== undefined) {
|
|
@@ -757,7 +834,9 @@ server.registerTool('obsrv_presets', {
|
|
|
757
834
|
title: 'List screen presets and panel profiles',
|
|
758
835
|
description: `List every screen preset (id, label, group, CSS dims, deviceScaleFactor, panel diagonal, derived physical ` +
|
|
759
836
|
`ppi) and panel profile (id, label, simulation params) accepted by obsrv_snap and obsrv_diff. Read straight ` +
|
|
760
|
-
`from the app's preset table — nothing is rendered
|
|
837
|
+
`from the app's preset table — nothing is rendered. The dimensions are each preset's natural orientation ` +
|
|
838
|
+
`(portrait for the mobile ones, landscape for the monitors); every preset also rotates — see the ` +
|
|
839
|
+
`\`orientation\` note in the result.`,
|
|
761
840
|
inputSchema: {},
|
|
762
841
|
outputSchema: presetsOutputShape,
|
|
763
842
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
package/out/preload/app.js
CHANGED
|
@@ -16,6 +16,7 @@ const IPC = {
|
|
|
16
16
|
frame: "obsrv:frame",
|
|
17
17
|
frameSubscribe: "obsrv:frame-subscribe",
|
|
18
18
|
urlChanged: "obsrv:url-changed",
|
|
19
|
+
titleChanged: "obsrv:title-changed",
|
|
19
20
|
loadError: "obsrv:load-error",
|
|
20
21
|
hostChanged: "obsrv:host-changed",
|
|
21
22
|
targetLoading: "obsrv:target-loading",
|
|
@@ -34,7 +35,12 @@ const IPC = {
|
|
|
34
35
|
updateStatus: "obsrv:update-status",
|
|
35
36
|
getHistory: "obsrv:get-history",
|
|
36
37
|
clearHistory: "obsrv:clear-history",
|
|
37
|
-
historyChanged: "obsrv:history-changed"
|
|
38
|
+
historyChanged: "obsrv:history-changed",
|
|
39
|
+
getTabs: "obsrv:get-tabs",
|
|
40
|
+
addTab: "obsrv:add-tab",
|
|
41
|
+
closeTab: "obsrv:close-tab",
|
|
42
|
+
activateTab: "obsrv:activate-tab",
|
|
43
|
+
tabsChanged: "obsrv:tabs-changed"
|
|
38
44
|
};
|
|
39
45
|
function subscribe(channel, cb) {
|
|
40
46
|
const listener = (_e, v) => cb(v);
|
|
@@ -69,24 +75,16 @@ const api = {
|
|
|
69
75
|
getSettings: () => electron.ipcRenderer.invoke(IPC.getSettings),
|
|
70
76
|
setSettings: (s) => electron.ipcRenderer.invoke(IPC.setSettings, s),
|
|
71
77
|
onFrame: subscribeFrames,
|
|
78
|
+
// Each of these names the tab it describes: main no longer gates them on the
|
|
79
|
+
// tab being in front, so a background tab keeps its own strip entry current
|
|
80
|
+
// without touching the address bar of the tab that is showing.
|
|
72
81
|
onUrlChanged: (cb) => subscribe(IPC.urlChanged, cb),
|
|
82
|
+
onTitleChanged: (cb) => subscribe(IPC.titleChanged, cb),
|
|
73
83
|
onLoadError: (cb) => subscribe(IPC.loadError, cb),
|
|
74
84
|
onHostChanged: (cb) => subscribe(IPC.hostChanged, cb),
|
|
75
85
|
onTargetLoading: (cb) => subscribe(IPC.targetLoading, cb),
|
|
76
|
-
onNativeFocused: (cb) =>
|
|
77
|
-
|
|
78
|
-
electron.ipcRenderer.on(IPC.nativeFocused, listener);
|
|
79
|
-
return () => {
|
|
80
|
-
electron.ipcRenderer.removeListener(IPC.nativeFocused, listener);
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
onTargetNavigating: (cb) => {
|
|
84
|
-
const listener = () => cb();
|
|
85
|
-
electron.ipcRenderer.on(IPC.targetNavigating, listener);
|
|
86
|
-
return () => {
|
|
87
|
-
electron.ipcRenderer.removeListener(IPC.targetNavigating, listener);
|
|
88
|
-
};
|
|
89
|
-
},
|
|
86
|
+
onNativeFocused: (cb) => subscribe(IPC.nativeFocused, cb),
|
|
87
|
+
onTargetNavigating: (cb) => subscribe(IPC.targetNavigating, cb),
|
|
90
88
|
onOpenImage: (cb) => {
|
|
91
89
|
const listener = () => cb();
|
|
92
90
|
electron.ipcRenderer.on(IPC.openImage, listener);
|
|
@@ -118,6 +116,11 @@ const api = {
|
|
|
118
116
|
onUpdateStatus: (cb) => subscribe(IPC.updateStatus, cb),
|
|
119
117
|
getHistory: () => electron.ipcRenderer.invoke(IPC.getHistory),
|
|
120
118
|
clearHistory: () => electron.ipcRenderer.invoke(IPC.clearHistory),
|
|
121
|
-
onHistoryChanged: (cb) => subscribe(IPC.historyChanged, cb)
|
|
119
|
+
onHistoryChanged: (cb) => subscribe(IPC.historyChanged, cb),
|
|
120
|
+
getTabs: () => electron.ipcRenderer.invoke(IPC.getTabs),
|
|
121
|
+
addTab: () => electron.ipcRenderer.invoke(IPC.addTab),
|
|
122
|
+
closeTab: (id) => electron.ipcRenderer.send(IPC.closeTab, id),
|
|
123
|
+
activateTab: (id) => electron.ipcRenderer.send(IPC.activateTab, id),
|
|
124
|
+
onTabsChanged: (cb) => subscribe(IPC.tabsChanged, cb)
|
|
122
125
|
};
|
|
123
126
|
electron.contextBridge.exposeInMainWorld("obsrv", api);
|