getobsrv 0.7.2 → 0.9.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 +43 -2
- package/bin/install-skill.js +113 -0
- package/bin/obsrv.js +8 -0
- package/out/cli/args.js +2 -0
- package/out/main/cli.js +2 -0
- 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-BeroS4wv.css → index-CasW01OP.css} +208 -80
- package/out/renderer/assets/{index-Cgx8iixu.js → index-CpeTWcl6.js} +529 -145
- 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
|
@@ -28,6 +28,38 @@ Both panes stay in lock-step (scroll, navigation) and the 1x pane is fully inter
|
|
|
28
28
|
fit), and are shown at true physical size — usually a small, dense render on a
|
|
29
29
|
desktop monitor, exactly like the phone in your hand.
|
|
30
30
|
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
**The desktop app** — download the DMG for your chip from
|
|
34
|
+
[Releases](https://github.com/vibesyemmy/obsrv/releases), drag Obsrv.app to
|
|
35
|
+
Applications, then clear the quarantine flag once (the build is not yet
|
|
36
|
+
notarised, so macOS falsely reports it as "damaged"):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
xattr -cr /Applications/Obsrv.app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Open it and set your monitor's diagonal in Settings — that one number is what
|
|
43
|
+
makes the target pane render at true physical size.
|
|
44
|
+
|
|
45
|
+
**The CLI, and Claude Code / MCP clients:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm i -g getobsrv # or use npx -y getobsrv
|
|
49
|
+
obsrv install-skill # teach Claude Code when to use it
|
|
50
|
+
claude mcp add --scope user obsrv -- npx -y getobsrv mcp # give it the tools
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`install-skill` copies the [obsrv-screens skill](skills/obsrv-screens/SKILL.md)
|
|
54
|
+
into `~/.claude/skills/` (`--dest` for elsewhere, `--print` to pipe it into
|
|
55
|
+
another agent framework). The skill is what makes an agent reach for Obsrv on
|
|
56
|
+
its own when frontend work needs checking; the MCP registration is what gives
|
|
57
|
+
it the tools to do so. New sessions pick both up.
|
|
58
|
+
|
|
59
|
+
**Both together** — install the app *and* the tools, then flip **Agent control**
|
|
60
|
+
on in the app's toolbar: agent testing now drives the window you are watching
|
|
61
|
+
instead of rendering invisibly.
|
|
62
|
+
|
|
31
63
|
## Use
|
|
32
64
|
|
|
33
65
|
```bash
|
|
@@ -47,6 +79,12 @@ middle-button drag, Option+drag or Option+wheel, or switch the toolbar's
|
|
|
47
79
|
pixel-exact — the footer says so) and click anywhere in it to jump back to 1:1
|
|
48
80
|
at that spot.
|
|
49
81
|
|
|
82
|
+
The `Both / Target` control beside it hides the native pane so the target render
|
|
83
|
+
takes the whole window — useful for a small mobile preset that would otherwise
|
|
84
|
+
sit in half a window, and for agent captures. The native pane stays loaded while
|
|
85
|
+
hidden, so the URL bar, back/forward and link clicks keep working exactly as
|
|
86
|
+
they do side by side.
|
|
87
|
+
|
|
50
88
|
## Agent & CI use
|
|
51
89
|
|
|
52
90
|
The same rendering pipeline runs headless — no window, JSON on stdout, humans
|
|
@@ -70,7 +108,8 @@ npx -y getobsrv diff http://localhost:5173 --preset laptop-768 --out-dir diffout
|
|
|
70
108
|
`npx -y getobsrv --help` (or `node bin/obsrv.js --help` in a checkout) lists every preset, profile and flag. Diff findings
|
|
71
109
|
are informational (exit 0); CI thresholds are the caller's job. A ready-made
|
|
72
110
|
Claude Code skill that wraps the loop (snap matrix → read the PNGs → diff →
|
|
73
|
-
fix → re-snap) lives at [skills/obsrv-screens/SKILL.md](skills/obsrv-screens/SKILL.md)
|
|
111
|
+
fix → re-snap) lives at [skills/obsrv-screens/SKILL.md](skills/obsrv-screens/SKILL.md);
|
|
112
|
+
`obsrv install-skill` copies it into `~/.claude/skills/` so agents find it.
|
|
74
113
|
|
|
75
114
|
### MCP server
|
|
76
115
|
|
|
@@ -83,7 +122,9 @@ raster density — the PNG comes back as an inline image up to 1.5 MiB),
|
|
|
83
122
|
If the desktop app is open with the toolbar's **Agent control** toggle on,
|
|
84
123
|
`obsrv_snap` drives the *visible* window instead: you watch the URL load and
|
|
85
124
|
the preset flip, and the agent gets back a capture of the app exactly as you
|
|
86
|
-
see it (plus `obsrv_drive` to flip URL/preset/profile directly
|
|
125
|
+
see it (plus `obsrv_drive` to flip URL/preset/profile/panes directly — `panes:
|
|
126
|
+
'target'` gives the target render the whole window, which is usually what you
|
|
127
|
+
want before a capture). Agents can
|
|
87
128
|
also scroll, click, pan and highlight while you watch — a drive session works
|
|
88
129
|
as a guided demo. A `scroll` reports the offset it actually reached
|
|
89
130
|
(`scrolled` / `scroller`), finds the inner scroll container on pages whose
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `obsrv install-skill` — copy the packaged Claude Code skill into the user's
|
|
3
|
+
// skills directory, so an agent picks up the snap → look → diff → fix loop
|
|
4
|
+
// without being told about it. Plain Node: no Electron, no build needed.
|
|
5
|
+
'use strict'
|
|
6
|
+
|
|
7
|
+
const { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } = require('node:fs')
|
|
8
|
+
const { homedir } = require('node:os')
|
|
9
|
+
const { join, resolve } = require('node:path')
|
|
10
|
+
|
|
11
|
+
const SKILL_NAME = 'obsrv-screens'
|
|
12
|
+
const source = join(__dirname, '..', 'skills', SKILL_NAME)
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
return `obsrv install-skill — install the ${SKILL_NAME} skill for Claude Code
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
obsrv install-skill [flags]
|
|
19
|
+
|
|
20
|
+
Flags:
|
|
21
|
+
--dest <dir> Skills directory to install into (default ~/.claude/skills).
|
|
22
|
+
--force Overwrite an existing, different copy.
|
|
23
|
+
--print Write SKILL.md to stdout instead of installing.
|
|
24
|
+
--help Show this message.
|
|
25
|
+
|
|
26
|
+
Installs to <dest>/${SKILL_NAME}/. New Claude Code sessions pick the skill up;
|
|
27
|
+
sessions already running need a restart.`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Copies a directory tree. Shallow enough for a skill (SKILL.md + optional references). */
|
|
31
|
+
function copyTree(from, to) {
|
|
32
|
+
mkdirSync(to, { recursive: true })
|
|
33
|
+
for (const entry of readdirSync(from)) {
|
|
34
|
+
const src = join(from, entry)
|
|
35
|
+
const dst = join(to, entry)
|
|
36
|
+
if (statSync(src).isDirectory()) copyTree(src, dst)
|
|
37
|
+
else copyFileSync(src, dst)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when every file in `from` exists in `to` with identical bytes. */
|
|
42
|
+
function sameTree(from, to) {
|
|
43
|
+
for (const entry of readdirSync(from)) {
|
|
44
|
+
const src = join(from, entry)
|
|
45
|
+
const dst = join(to, entry)
|
|
46
|
+
if (!existsSync(dst)) return false
|
|
47
|
+
if (statSync(src).isDirectory()) {
|
|
48
|
+
if (!statSync(dst).isDirectory() || !sameTree(src, dst)) return false
|
|
49
|
+
} else if (!readFileSync(src).equals(readFileSync(dst))) return false
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function main(argv) {
|
|
55
|
+
let dest = join(homedir(), '.claude', 'skills')
|
|
56
|
+
let force = false
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const flag = argv[i]
|
|
60
|
+
if (flag === '--help' || flag === '-h') {
|
|
61
|
+
console.log(usage())
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
if (flag === '--print') {
|
|
65
|
+
process.stdout.write(readFileSync(join(source, 'SKILL.md'), 'utf8'))
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
if (flag === '--force') {
|
|
69
|
+
force = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (flag === '--dest') {
|
|
73
|
+
const value = argv[++i]
|
|
74
|
+
if (!value) {
|
|
75
|
+
console.error('obsrv install-skill: --dest needs a directory')
|
|
76
|
+
return 2
|
|
77
|
+
}
|
|
78
|
+
dest = resolve(value)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
console.error(`obsrv install-skill: unknown flag: ${flag}\n\n${usage()}`)
|
|
82
|
+
return 2
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!existsSync(source)) {
|
|
86
|
+
console.error(`obsrv install-skill: the packaged skill is missing (looked in ${source})`)
|
|
87
|
+
return 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const target = join(dest, SKILL_NAME)
|
|
91
|
+
if (existsSync(target)) {
|
|
92
|
+
if (sameTree(source, target)) {
|
|
93
|
+
console.error(`obsrv install-skill: already up to date at ${target}`)
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
if (!force) {
|
|
97
|
+
console.error(
|
|
98
|
+
`obsrv install-skill: ${target} exists and differs — pass --force to overwrite it, ` +
|
|
99
|
+
'or --dest to install elsewhere',
|
|
100
|
+
)
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
copyTree(source, target)
|
|
106
|
+
console.error(
|
|
107
|
+
`obsrv install-skill: installed to ${target}\n` +
|
|
108
|
+
'New Claude Code sessions will pick it up; restart any session already running.',
|
|
109
|
+
)
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(main(process.argv.slice(2)))
|
package/bin/obsrv.js
CHANGED
|
@@ -20,6 +20,14 @@ if (process.argv[2] === 'mcp') {
|
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// `obsrv install-skill` copies the packaged Claude Code skill into the user's
|
|
24
|
+
// skills directory. Also plain node — it never renders anything.
|
|
25
|
+
if (process.argv[2] === 'install-skill') {
|
|
26
|
+
process.argv.splice(2, 1)
|
|
27
|
+
require('./install-skill.js')
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
const cliEntry = join(__dirname, '..', 'out', 'main', 'cli.js')
|
|
24
32
|
if (!existsSync(cliEntry)) {
|
|
25
33
|
console.error('obsrv: out/main/cli.js is missing — run `npm run build` in the Obsrv repo first')
|
package/out/cli/args.js
CHANGED
|
@@ -22,6 +22,8 @@ function usage() {
|
|
|
22
22
|
Usage:
|
|
23
23
|
obsrv snap <url> [flags] Render <url> on a target screen; write a PNG, print JSON.
|
|
24
24
|
obsrv diff <url> [flags] Render <url> at 1x and against a 2x reference; print JSON metrics.
|
|
25
|
+
obsrv mcp Serve the MCP server on stdio (for Claude Code and other clients).
|
|
26
|
+
obsrv install-skill Install the obsrv-screens skill for Claude Code (--help for flags).
|
|
25
27
|
|
|
26
28
|
Shared flags:
|
|
27
29
|
--preset <id> Screen preset (default ${exports.DEFAULT_PRESET}):
|
package/out/main/cli.js
CHANGED
|
@@ -53,6 +53,8 @@ function usage() {
|
|
|
53
53
|
Usage:
|
|
54
54
|
obsrv snap <url> [flags] Render <url> on a target screen; write a PNG, print JSON.
|
|
55
55
|
obsrv diff <url> [flags] Render <url> at 1x and against a 2x reference; print JSON metrics.
|
|
56
|
+
obsrv mcp Serve the MCP server on stdio (for Claude Code and other clients).
|
|
57
|
+
obsrv install-skill Install the obsrv-screens skill for Claude Code (--help for flags).
|
|
56
58
|
|
|
57
59
|
Shared flags:
|
|
58
60
|
--preset <id> Screen preset (default ${DEFAULT_PRESET}):
|
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: "1:1", mode: "url" };
|
|
795
|
+
const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", 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);
|