getobsrv 0.10.0 → 0.12.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/main/cli.js +1 -1
- package/out/main/index.js +842 -192
- package/out/main/{targetSource-CbJwfugi.js → targetSource-DlsHc-Ri.js} +50 -1
- package/out/mcp/server.js +21 -1
- package/out/preload/app.js +25 -16
- package/out/renderer/assets/{index-CZUlXhEb.css → index-DvwDBeho.css} +284 -14
- package/out/renderer/assets/{index-CQVE5hF3.js → index-gYDbIgqJ.js} +655 -157
- package/out/renderer/index.html +2 -2
- package/out/shared/control.js +13 -1
- package/out/shared/history.js +90 -0
- package/out/shared/ipcPayloads.js +41 -2
- package/out/shared/presets.js +12 -1
- package/out/shared/tabList.js +46 -0
- package/package.json +1 -1
- package/skills/obsrv-screens/SKILL.md +7 -0
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-gYDbIgqJ.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DvwDBeho.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/out/shared/control.js
CHANGED
|
@@ -233,5 +233,17 @@ function parseControlStatus(raw) {
|
|
|
233
233
|
const panes = raw.panes ?? 'both';
|
|
234
234
|
if (panes !== 'both' && panes !== 'target')
|
|
235
235
|
return null;
|
|
236
|
-
|
|
236
|
+
// The same skew, one release later. An app that predates tabs has exactly
|
|
237
|
+
// one session — it is the tab at index 0, and it has no id to name — so the
|
|
238
|
+
// defaults describe that app truthfully rather than papering over it. An
|
|
239
|
+
// agent polling one sees an unchanging `tabId`, which is correct: there is
|
|
240
|
+
// no other tab for the user to switch to. Returning null instead would take
|
|
241
|
+
// out drive and live snap wholesale against every app older than tabs.
|
|
242
|
+
const tabId = raw.tabId ?? '';
|
|
243
|
+
if (typeof tabId !== 'string')
|
|
244
|
+
return null;
|
|
245
|
+
const tabIndex = raw.tabIndex ?? 0;
|
|
246
|
+
if (typeof tabIndex !== 'number' || !Number.isInteger(tabIndex) || tabIndex < 0)
|
|
247
|
+
return null;
|
|
248
|
+
return { version, url, presetId, profileId, viewMode, panes, mode, tabId, tabIndex };
|
|
237
249
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isStamp = exports.isCount = exports.isRecord = exports.MAX_URL_LENGTH = exports.HISTORY_SUGGESTIONS = exports.HISTORY_MAX = void 0;
|
|
4
|
+
exports.isStorableUrl = isStorableUrl;
|
|
5
|
+
exports.byRank = byRank;
|
|
6
|
+
exports.recordVisit = recordVisit;
|
|
7
|
+
exports.matchHistory = matchHistory;
|
|
8
|
+
const url_1 = require("./url");
|
|
9
|
+
/**
|
|
10
|
+
* The file is read and written whole, so it needs a bound. 500 is far beyond
|
|
11
|
+
* what this app's usage produces — a handful of dev and staging addresses
|
|
12
|
+
* returned to over and over — and small enough to parse instantly.
|
|
13
|
+
*/
|
|
14
|
+
exports.HISTORY_MAX = 500;
|
|
15
|
+
/** Rows the URL bar offers. Past six, a longer list is a worse tool than a better query. */
|
|
16
|
+
exports.HISTORY_SUGGESTIONS = 6;
|
|
17
|
+
/**
|
|
18
|
+
* The other half of the bound: 500 entries of unbounded length is not a
|
|
19
|
+
* bounded file. Far longer than any address this tool is pointed at, and
|
|
20
|
+
* short enough that the whole file stays trivial to parse.
|
|
21
|
+
*/
|
|
22
|
+
exports.MAX_URL_LENGTH = 2048;
|
|
23
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
24
|
+
exports.isRecord = isRecord;
|
|
25
|
+
const isCount = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 1;
|
|
26
|
+
exports.isCount = isCount;
|
|
27
|
+
const isStamp = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
28
|
+
exports.isStamp = isStamp;
|
|
29
|
+
/**
|
|
30
|
+
* Is this an address worth remembering? Only the schemes the app can load
|
|
31
|
+
* back (`shared/url.ts`), so `about:blank` — which the native pane commits on
|
|
32
|
+
* every launch — never becomes a suggestion, and a scheme the URL bar would
|
|
33
|
+
* refuse can never be offered by it.
|
|
34
|
+
*/
|
|
35
|
+
function isStorableUrl(v) {
|
|
36
|
+
if (typeof v !== 'string' || v === '' || v.length > exports.MAX_URL_LENGTH)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
return url_1.ALLOWED_URL_SCHEMES.includes(new URL(v).protocol);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Most recent first, visit count breaking ties — so the address used
|
|
47
|
+
* constantly stays at the top without a stale favourite outranking what was
|
|
48
|
+
* on screen ten minutes ago. URL breaks the remaining tie only to keep the
|
|
49
|
+
* order stable across reloads of the same file.
|
|
50
|
+
*/
|
|
51
|
+
function byRank(a, b) {
|
|
52
|
+
if (a.lastVisit !== b.lastVisit)
|
|
53
|
+
return b.lastVisit - a.lastVisit;
|
|
54
|
+
if (a.visits !== b.visits)
|
|
55
|
+
return b.visits - a.visits;
|
|
56
|
+
return a.url < b.url ? -1 : a.url > b.url ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The list after a committed navigation to `url`. Returns `entries`
|
|
60
|
+
* *unchanged* — the same reference — when the URL is not one we store, which
|
|
61
|
+
* is what lets the caller write the file only when something actually
|
|
62
|
+
* changed.
|
|
63
|
+
*
|
|
64
|
+
* Eviction is by rank, so the least recently visited entry is the one that
|
|
65
|
+
* goes; a busy day cannot push out the address visited an hour ago in favour
|
|
66
|
+
* of one visited last year.
|
|
67
|
+
*/
|
|
68
|
+
function recordVisit(entries, url, now) {
|
|
69
|
+
if (!isStorableUrl(url) || !(0, exports.isStamp)(now))
|
|
70
|
+
return entries;
|
|
71
|
+
const previous = entries.find(e => e.url === url);
|
|
72
|
+
const next = {
|
|
73
|
+
url,
|
|
74
|
+
visits: previous ? previous.visits + 1 : 1,
|
|
75
|
+
lastVisit: now,
|
|
76
|
+
};
|
|
77
|
+
return [next, ...entries.filter(e => e.url !== url)].sort(byRank).slice(0, exports.HISTORY_MAX);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The rows to offer for what has been typed: case-insensitive substring
|
|
81
|
+
* against the whole URL, in rank order. An empty query matches everything,
|
|
82
|
+
* so pressing Down in an empty field offers the most recent addresses.
|
|
83
|
+
*/
|
|
84
|
+
function matchHistory(entries, query, limit = exports.HISTORY_SUGGESTIONS) {
|
|
85
|
+
const needle = query.trim().toLowerCase();
|
|
86
|
+
return entries
|
|
87
|
+
.filter(e => e.url.toLowerCase().includes(needle))
|
|
88
|
+
.sort(byRank)
|
|
89
|
+
.slice(0, Math.max(0, limit));
|
|
90
|
+
}
|
|
@@ -6,10 +6,12 @@ exports.parseInputEvent = parseInputEvent;
|
|
|
6
6
|
exports.parseDeviceScaleFactor = parseDeviceScaleFactor;
|
|
7
7
|
exports.parseSettings = parseSettings;
|
|
8
8
|
exports.parseMode = parseMode;
|
|
9
|
+
exports.parseTabId = parseTabId;
|
|
9
10
|
exports.parseUiState = parseUiState;
|
|
10
11
|
exports.parseScrollPos = parseScrollPos;
|
|
11
12
|
exports.parseScrollRequest = parseScrollRequest;
|
|
12
13
|
exports.parseScrollReport = parseScrollReport;
|
|
14
|
+
const presets_1 = require("./presets");
|
|
13
15
|
const types_1 = require("./types");
|
|
14
16
|
/**
|
|
15
17
|
* Parsers for everything the renderer sends main over IPC. Each returns a
|
|
@@ -99,7 +101,7 @@ function parseDeviceScaleFactor(raw) {
|
|
|
99
101
|
return raw;
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
|
-
* Copies exactly the
|
|
104
|
+
* Copies exactly the known keys; the numbers must be finite and
|
|
103
105
|
* positive. A missing `agentControl` means false and a missing `updateCheck`
|
|
104
106
|
* means true (the pre-feature wire shapes); any non-boolean value is refused,
|
|
105
107
|
* never coerced.
|
|
@@ -121,13 +123,43 @@ function parseSettings(raw) {
|
|
|
121
123
|
const lastUpdateCheck = raw.lastUpdateCheck ?? 0;
|
|
122
124
|
if (!isFiniteNumber(lastUpdateCheck) || lastUpdateCheck < 0)
|
|
123
125
|
return null;
|
|
124
|
-
|
|
126
|
+
const recordHistory = raw.recordHistory ?? true;
|
|
127
|
+
if (typeof recordHistory !== 'boolean')
|
|
128
|
+
return null;
|
|
129
|
+
// A missing `split` is the pre-feature wire shape and means the default.
|
|
130
|
+
// Out of band it is refused rather than clamped: `loadSettings` forgives a
|
|
131
|
+
// hand-edited file because it must, but the renderer clamps before it
|
|
132
|
+
// sends, so a bad ratio arriving here is a bug worth surfacing.
|
|
133
|
+
const split = raw.split ?? presets_1.DEFAULT_SETTINGS.split;
|
|
134
|
+
if (!isFiniteNumber(split) || split < presets_1.SPLIT_MIN || split > presets_1.SPLIT_MAX)
|
|
135
|
+
return null;
|
|
136
|
+
// Same shape for the tab cap: absent is the pre-tabs wire shape and means
|
|
137
|
+
// the default, and an out-of-band or fractional count is refused rather
|
|
138
|
+
// than clamped — the Settings input is a bounded integer field, so one
|
|
139
|
+
// arriving here is a bug rather than a hand-edited file.
|
|
140
|
+
const maxTabs = raw.maxTabs ?? presets_1.DEFAULT_SETTINGS.maxTabs;
|
|
141
|
+
if (!isFiniteNumber(maxTabs) || !Number.isInteger(maxTabs) || maxTabs < presets_1.MAX_TABS_MIN || maxTabs > presets_1.MAX_TABS_MAX)
|
|
142
|
+
return null;
|
|
143
|
+
return { hostDiagonalInches, hostNits, agentControl, updateCheck, lastUpdateCheck, recordHistory, split, maxTabs };
|
|
125
144
|
}
|
|
126
145
|
function parseMode(raw) {
|
|
127
146
|
return raw === 'url' || raw === 'image' ? raw : null;
|
|
128
147
|
}
|
|
129
148
|
/** Longest preset/profile id the UI-state mirror will store. */
|
|
130
149
|
const MAX_UI_ID = 64;
|
|
150
|
+
/**
|
|
151
|
+
* Longest tab id main will act on. Ids are minted main-side (`tab-N`), so a
|
|
152
|
+
* renderer message naming one is only ever echoing what it was told; the bound
|
|
153
|
+
* is there because the renderer is still the one sending it. An id no session
|
|
154
|
+
* carries resolves to nothing in the manager and the command is dropped, so
|
|
155
|
+
* shape is all this has to check.
|
|
156
|
+
*/
|
|
157
|
+
const MAX_TAB_ID = 64;
|
|
158
|
+
function parseTabId(raw) {
|
|
159
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.length > MAX_TAB_ID)
|
|
160
|
+
return null;
|
|
161
|
+
return raw;
|
|
162
|
+
}
|
|
131
163
|
/**
|
|
132
164
|
* The renderer's UI-state report (`IPC.uiState`), mirrored main-side so the
|
|
133
165
|
* agent-control server can answer `status` without a renderer round-trip.
|
|
@@ -145,6 +177,12 @@ function parseUiState(raw) {
|
|
|
145
177
|
if (!isRecord(raw))
|
|
146
178
|
return null;
|
|
147
179
|
const { presetId, profileId, viewMode, mode } = raw;
|
|
180
|
+
// Required, unlike `panes` below: there is no sane default for "which tab
|
|
181
|
+
// this describes", and guessing would reintroduce exactly the misattribution
|
|
182
|
+
// the field exists to stop.
|
|
183
|
+
const tabId = parseTabId(raw.tabId);
|
|
184
|
+
if (tabId === null)
|
|
185
|
+
return null;
|
|
148
186
|
if (typeof presetId !== 'string' || presetId.length === 0 || presetId.length > MAX_UI_ID)
|
|
149
187
|
return null;
|
|
150
188
|
if (typeof profileId !== 'string' || profileId.length === 0 || profileId.length > MAX_UI_ID)
|
|
@@ -160,6 +198,7 @@ function parseUiState(raw) {
|
|
|
160
198
|
if (panes !== 'both' && panes !== 'target')
|
|
161
199
|
return null;
|
|
162
200
|
return {
|
|
201
|
+
tabId,
|
|
163
202
|
presetId,
|
|
164
203
|
profileId,
|
|
165
204
|
viewMode,
|
package/out/shared/presets.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exports.MAX_VIEWPORT = void 0;
|
|
3
|
+
exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exports.MAX_TABS_MAX = exports.MAX_TABS_MIN = exports.SPLIT_MAX = exports.SPLIT_MIN = exports.MAX_VIEWPORT = void 0;
|
|
4
4
|
exports.findPreset = findPreset;
|
|
5
5
|
exports.findProfile = findProfile;
|
|
6
6
|
exports.MAX_VIEWPORT = 4096;
|
|
7
|
+
/** The loose sanity band for `Settings.split`; the clamp that matters is the
|
|
8
|
+
* drag's 240px-a-side floor, which only the renderer knows the pixels for. */
|
|
9
|
+
exports.SPLIT_MIN = 0.1;
|
|
10
|
+
exports.SPLIT_MAX = 0.9;
|
|
11
|
+
/** The band `Settings.maxTabs` is held to. Below two there is nothing to tab
|
|
12
|
+
* between; above thirty-two the process count is a problem on any machine. */
|
|
13
|
+
exports.MAX_TABS_MIN = 2;
|
|
14
|
+
exports.MAX_TABS_MAX = 32;
|
|
7
15
|
exports.DEFAULT_SETTINGS = {
|
|
8
16
|
hostDiagonalInches: 27,
|
|
9
17
|
hostNits: 500,
|
|
10
18
|
agentControl: false,
|
|
11
19
|
updateCheck: true,
|
|
12
20
|
lastUpdateCheck: 0,
|
|
21
|
+
recordHistory: true,
|
|
22
|
+
split: 0.5,
|
|
23
|
+
maxTabs: 12,
|
|
13
24
|
};
|
|
14
25
|
exports.SCREEN_PRESETS = [
|
|
15
26
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.closeTab = closeTab;
|
|
4
|
+
exports.canAddTab = canAddTab;
|
|
5
|
+
exports.tabTitle = tabTitle;
|
|
6
|
+
/**
|
|
7
|
+
* Removes a tab and says which one should take focus. Closing the active tab
|
|
8
|
+
* moves right, because the tab that took its screen position is the one the
|
|
9
|
+
* eye is already on; at the end of the strip there is nothing to the right, so
|
|
10
|
+
* it moves left.
|
|
11
|
+
*/
|
|
12
|
+
function closeTab(tabs, closeId, activeId) {
|
|
13
|
+
const index = tabs.findIndex(t => t.id === closeId);
|
|
14
|
+
if (index === -1)
|
|
15
|
+
return { tabs, activeId };
|
|
16
|
+
const next = tabs.filter(t => t.id !== closeId);
|
|
17
|
+
if (next.length === 0)
|
|
18
|
+
return { tabs: next, activeId: null };
|
|
19
|
+
if (closeId !== activeId)
|
|
20
|
+
return { tabs: next, activeId };
|
|
21
|
+
const neighbour = next[Math.min(index, next.length - 1)];
|
|
22
|
+
return { tabs: next, activeId: neighbour.id };
|
|
23
|
+
}
|
|
24
|
+
function canAddTab(count, max) {
|
|
25
|
+
return count < max;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What the strip shows. The port is kept deliberately: two local dev servers
|
|
29
|
+
* differ only by it, and a host-only label would render them identically.
|
|
30
|
+
*/
|
|
31
|
+
function tabTitle(url, pageTitle) {
|
|
32
|
+
if (pageTitle.trim() !== '')
|
|
33
|
+
return pageTitle;
|
|
34
|
+
// `about:blank` is where every session starts and what an unused tab still
|
|
35
|
+
// holds, so it is the empty tab spelled in Chromium's words rather than an
|
|
36
|
+
// address anyone typed. It has no host, and falling through to the raw
|
|
37
|
+
// string labels a fresh tab `about:blank` — which the screenshot caught.
|
|
38
|
+
if (url.trim() === '' || url.trim() === 'about:blank')
|
|
39
|
+
return 'New tab';
|
|
40
|
+
try {
|
|
41
|
+
return new URL(url).host || url;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -61,6 +61,13 @@ when the PNG is taken. Reaching for `obsrv_snap` after a scroll works only
|
|
|
61
61
|
when the app is already on that exact URL (it answers `navigated: false`);
|
|
62
62
|
snapping a different URL is a fresh load and lands back at the top.
|
|
63
63
|
|
|
64
|
+
The app can hold several sessions open as tabs, and both tools act on the one
|
|
65
|
+
in **front** — resolved per command, so the user can move it under you. Each
|
|
66
|
+
result names the tab (`tabId`, `tabIndex`); if you drive over several calls and
|
|
67
|
+
the state has to hold, check that `tabId` did not change rather than assuming
|
|
68
|
+
it. You cannot name another tab, and you cannot open, close or switch tabs —
|
|
69
|
+
ask the user. An empty `tabId` is an app older than tabs, which has only one.
|
|
70
|
+
|
|
64
71
|
## The loop that catches real regressions
|
|
65
72
|
|
|
66
73
|
1. Snap the dev URL across `--matrix laptop-768,android-65,1080p-24`, plus a
|