@ddwang/magnitude-core 0.3.1-ddwang.1
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/dist/actions/desktopActions.d.ts +58 -0
- package/dist/actions/desktopActions.js +154 -0
- package/dist/actions/index.d.ts +26 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/taskActions.d.ts +8 -0
- package/dist/actions/taskActions.js +27 -0
- package/dist/actions/types.d.ts +31 -0
- package/dist/actions/types.js +5 -0
- package/dist/actions/util.d.ts +20 -0
- package/dist/actions/util.js +297 -0
- package/dist/actions/webActions.d.ts +79 -0
- package/dist/actions/webActions.js +190 -0
- package/dist/agent/browserAgent.d.ts +29 -0
- package/dist/agent/browserAgent.js +122 -0
- package/dist/agent/errors.d.ts +8 -0
- package/dist/agent/errors.js +68 -0
- package/dist/agent/index.d.ts +53 -0
- package/dist/agent/index.js +377 -0
- package/dist/agent/narrator.d.ts +4 -0
- package/dist/agent/narrator.js +63 -0
- package/dist/agent/pause.test.d.ts +1 -0
- package/dist/agent/pause.test.js +194 -0
- package/dist/ai/baml_client/async_client.d.ts +73 -0
- package/dist/ai/baml_client/async_client.js +152 -0
- package/dist/ai/baml_client/async_request.d.ts +35 -0
- package/dist/ai/baml_client/async_request.js +98 -0
- package/dist/ai/baml_client/config.d.ts +11 -0
- package/dist/ai/baml_client/config.js +20 -0
- package/dist/ai/baml_client/globals.d.ts +16 -0
- package/dist/ai/baml_client/globals.js +37 -0
- package/dist/ai/baml_client/index.d.ts +16 -0
- package/dist/ai/baml_client/index.js +41 -0
- package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
- package/dist/ai/baml_client/inlinedbaml.js +31 -0
- package/dist/ai/baml_client/parser.d.ts +53 -0
- package/dist/ai/baml_client/parser.js +86 -0
- package/dist/ai/baml_client/partial_types.d.ts +61 -0
- package/dist/ai/baml_client/partial_types.js +10 -0
- package/dist/ai/baml_client/sync_client.d.ts +50 -0
- package/dist/ai/baml_client/sync_client.js +100 -0
- package/dist/ai/baml_client/sync_request.d.ts +35 -0
- package/dist/ai/baml_client/sync_request.js +98 -0
- package/dist/ai/baml_client/tracing.d.ts +16 -0
- package/dist/ai/baml_client/tracing.js +18 -0
- package/dist/ai/baml_client/type_builder.d.ts +38 -0
- package/dist/ai/baml_client/type_builder.js +96 -0
- package/dist/ai/baml_client/types.d.ts +69 -0
- package/dist/ai/baml_client/types.js +15 -0
- package/dist/ai/claudeCode.d.ts +1 -0
- package/dist/ai/claudeCode.js +186 -0
- package/dist/ai/modelHarness.d.ts +39 -0
- package/dist/ai/modelHarness.js +189 -0
- package/dist/ai/multiModelHarness.d.ts +27 -0
- package/dist/ai/multiModelHarness.js +52 -0
- package/dist/ai/types.d.ts +90 -0
- package/dist/ai/types.js +5 -0
- package/dist/ai/util.d.ts +13 -0
- package/dist/ai/util.js +165 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +3 -0
- package/dist/common/actions.d.ts +0 -0
- package/dist/common/actions.js +14 -0
- package/dist/common/events.d.ts +15 -0
- package/dist/common/events.js +3 -0
- package/dist/common/failure.d.ts +68 -0
- package/dist/common/failure.js +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/retry.d.ts +16 -0
- package/dist/common/retry.js +30 -0
- package/dist/common/util.d.ts +23 -0
- package/dist/common/util.js +132 -0
- package/dist/connectors/browserConnector.d.ts +40 -0
- package/dist/connectors/browserConnector.js +127 -0
- package/dist/connectors/desktopConnector.d.ts +58 -0
- package/dist/connectors/desktopConnector.js +68 -0
- package/dist/connectors/index.d.ts +10 -0
- package/dist/connectors/index.js +2 -0
- package/dist/index.cjs +6143 -0
- package/dist/index.d.cts +825 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +6097 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +15 -0
- package/dist/memory/agentMemory.d.ts +37 -0
- package/dist/memory/agentMemory.js +121 -0
- package/dist/memory/image.d.ts +26 -0
- package/dist/memory/image.js +106 -0
- package/dist/memory/index.d.ts +3 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/masking.d.ts +7 -0
- package/dist/memory/masking.js +141 -0
- package/dist/memory/masking.test.d.ts +1 -0
- package/dist/memory/masking.test.js +117 -0
- package/dist/memory/observation.d.ts +38 -0
- package/dist/memory/observation.js +79 -0
- package/dist/memory/rendering/index.d.ts +10 -0
- package/dist/memory/rendering/index.js +13 -0
- package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
- package/dist/memory/rendering/renderJsonParts.js +115 -0
- package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
- package/dist/memory/rendering/renderJsonParts.test.js +217 -0
- package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
- package/dist/memory/rendering/renderXmlParts.js +127 -0
- package/dist/memory/serde.d.ts +20 -0
- package/dist/memory/serde.js +81 -0
- package/dist/memory/util.d.ts +2 -0
- package/dist/memory/util.js +17 -0
- package/dist/telemetry/events.d.ts +9 -0
- package/dist/telemetry/events.js +80 -0
- package/dist/telemetry/index.d.ts +6 -0
- package/dist/telemetry/index.js +91 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +12 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/web/browserProvider.d.ts +23 -0
- package/dist/web/browserProvider.js +153 -0
- package/dist/web/harness.d.ts +102 -0
- package/dist/web/harness.js +340 -0
- package/dist/web/stability.d.ts +57 -0
- package/dist/web/stability.js +316 -0
- package/dist/web/tabs.d.ts +58 -0
- package/dist/web/tabs.js +293 -0
- package/dist/web/transformer.d.ts +7 -0
- package/dist/web/transformer.js +41 -0
- package/dist/web/types.d.ts +32 -0
- package/dist/web/types.js +2 -0
- package/dist/web/util.d.ts +3 -0
- package/dist/web/util.js +81 -0
- package/dist/web/visualizer/cursor.d.ts +16 -0
- package/dist/web/visualizer/cursor.js +152 -0
- package/dist/web/visualizer/index.d.ts +22 -0
- package/dist/web/visualizer/index.js +47 -0
- package/dist/web/visualizer/mouseEffects.d.ts +18 -0
- package/dist/web/visualizer/mouseEffects.js +287 -0
- package/dist/web/visualizer/typeEffects.d.ts +10 -0
- package/dist/web/visualizer/typeEffects.js +190 -0
- package/package.json +71 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import logger from '@/logger';
|
|
3
|
+
// Maximum wait time for page stability in ms
|
|
4
|
+
const DEFAULT_PAGE_STABILITY_TIMEOUT = 5000;
|
|
5
|
+
// Default minimum wait time for page load
|
|
6
|
+
const DEFAULT_MINIMUM_WAIT_PAGE_LOAD_TIME = 500;
|
|
7
|
+
// Default time to wait for network requests to finish
|
|
8
|
+
const DEFAULT_WAIT_FOR_NETWORK_IDLE_TIME = 500;
|
|
9
|
+
// Default maximum time to wait for page load
|
|
10
|
+
const DEFAULT_MAXIMUM_WAIT_PAGE_LOAD_TIME = 5000;
|
|
11
|
+
/**
|
|
12
|
+
* Utility class to check for visual stability of a page
|
|
13
|
+
*/
|
|
14
|
+
export class PageStabilityAnalyzer {
|
|
15
|
+
page;
|
|
16
|
+
options;
|
|
17
|
+
lastStart;
|
|
18
|
+
logger;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
//this.page = page;
|
|
21
|
+
this.options = {
|
|
22
|
+
differenceThreshold: options.differenceThreshold ?? 0.01,
|
|
23
|
+
requiredStableChecks: options.requiredStableChecks ?? 3,
|
|
24
|
+
checkInterval: options.checkInterval ?? 100, // 200ms
|
|
25
|
+
minimumWaitPageLoadTime: options.minimumWaitPageLoadTime ?? DEFAULT_MINIMUM_WAIT_PAGE_LOAD_TIME,
|
|
26
|
+
waitForNetworkIdleTime: options.waitForNetworkIdleTime ?? DEFAULT_WAIT_FOR_NETWORK_IDLE_TIME,
|
|
27
|
+
maximumWaitPageLoadTime: options.maximumWaitPageLoadTime ?? DEFAULT_MAXIMUM_WAIT_PAGE_LOAD_TIME,
|
|
28
|
+
disableVisualStability: options.disableVisualStability ?? false
|
|
29
|
+
};
|
|
30
|
+
this.lastStart = Date.now();
|
|
31
|
+
this.logger = logger.child({ name: 'magnus.stability' });
|
|
32
|
+
}
|
|
33
|
+
setActivePage(page) {
|
|
34
|
+
this.page = page;
|
|
35
|
+
}
|
|
36
|
+
log(message) {
|
|
37
|
+
this.logger.trace(`[${Date.now() - this.lastStart}ms] ${message}`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compare two screenshots and return their difference score
|
|
41
|
+
* @returns Difference as float between 0-1, where 0 means identical
|
|
42
|
+
*/
|
|
43
|
+
async compareScreenshots(screenshot1, screenshot2) {
|
|
44
|
+
try {
|
|
45
|
+
// Convert screenshots to raw pixel data using Sharp
|
|
46
|
+
const img1 = await sharp(screenshot1).raw().toBuffer({ resolveWithObject: true });
|
|
47
|
+
const img2 = await sharp(screenshot2).raw().toBuffer({ resolveWithObject: true });
|
|
48
|
+
// Check for size mismatch
|
|
49
|
+
if (img1.info.width !== img2.info.width || img1.info.height !== img2.info.height) {
|
|
50
|
+
return {
|
|
51
|
+
difference: 1.0,
|
|
52
|
+
error: "Image sizes don't match"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Calculate pixel differences
|
|
56
|
+
let diffSum = 0;
|
|
57
|
+
for (let i = 0; i < img1.data.length; i++) {
|
|
58
|
+
diffSum += Math.abs(img1.data[i] - img2.data[i]);
|
|
59
|
+
}
|
|
60
|
+
// Calculate mean difference and normalize
|
|
61
|
+
const mse = diffSum / img1.data.length;
|
|
62
|
+
// Normalize to 0-1 range (assuming 8-bit color depth)
|
|
63
|
+
const maxDiff = 255.0 * (img1.info.channels || 3); // Multiply by number of channels
|
|
64
|
+
const normalizedDiff = mse / maxDiff;
|
|
65
|
+
return { difference: normalizedDiff };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return {
|
|
69
|
+
difference: 1.0,
|
|
70
|
+
error: `Comparison failed: ${e instanceof Error ? e.message : String(e)}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wait for the network to become stable
|
|
76
|
+
* This monitors network requests and waits until there's a period of inactivity
|
|
77
|
+
* @param timeout Maximum time to wait for network stability
|
|
78
|
+
*/
|
|
79
|
+
async waitForNetworkStability(timeout) {
|
|
80
|
+
const maxWaitTime = timeout ?? this.options.maximumWaitPageLoadTime;
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
this.log("Checking network stability");
|
|
83
|
+
const pendingRequests = new Set();
|
|
84
|
+
let lastActivity = Date.now();
|
|
85
|
+
// Define relevant resource types and content types
|
|
86
|
+
const RELEVANT_RESOURCE_TYPES = new Set([
|
|
87
|
+
'document',
|
|
88
|
+
'stylesheet',
|
|
89
|
+
'image',
|
|
90
|
+
'font',
|
|
91
|
+
'script',
|
|
92
|
+
'fetch',
|
|
93
|
+
'xhr',
|
|
94
|
+
'iframe'
|
|
95
|
+
]);
|
|
96
|
+
const RELEVANT_CONTENT_TYPES = [
|
|
97
|
+
'text/html',
|
|
98
|
+
'text/css',
|
|
99
|
+
'application/javascript',
|
|
100
|
+
'image/',
|
|
101
|
+
'font/',
|
|
102
|
+
'application/json'
|
|
103
|
+
];
|
|
104
|
+
// Additional patterns to filter out
|
|
105
|
+
const IGNORED_URL_PATTERNS = [
|
|
106
|
+
// Analytics and tracking
|
|
107
|
+
'analytics',
|
|
108
|
+
'tracking',
|
|
109
|
+
'telemetry',
|
|
110
|
+
'beacon',
|
|
111
|
+
'metrics',
|
|
112
|
+
// Ad-related
|
|
113
|
+
'doubleclick',
|
|
114
|
+
'adsystem',
|
|
115
|
+
'adserver',
|
|
116
|
+
'advertising',
|
|
117
|
+
// Social media widgets
|
|
118
|
+
'facebook.com/plugins',
|
|
119
|
+
'platform.twitter',
|
|
120
|
+
'linkedin.com/embed',
|
|
121
|
+
// Live chat and support
|
|
122
|
+
'livechat',
|
|
123
|
+
'zendesk',
|
|
124
|
+
'intercom',
|
|
125
|
+
'crisp.chat',
|
|
126
|
+
'hotjar',
|
|
127
|
+
// Push notifications
|
|
128
|
+
'push-notifications',
|
|
129
|
+
'onesignal',
|
|
130
|
+
'pushwoosh',
|
|
131
|
+
// Background sync/heartbeat
|
|
132
|
+
'heartbeat',
|
|
133
|
+
'ping',
|
|
134
|
+
'alive',
|
|
135
|
+
// WebRTC and streaming
|
|
136
|
+
'webrtc',
|
|
137
|
+
'rtmp://',
|
|
138
|
+
'wss://',
|
|
139
|
+
// Common CDNs for dynamic content
|
|
140
|
+
'cloudfront.net',
|
|
141
|
+
'fastly.net'
|
|
142
|
+
];
|
|
143
|
+
// Setup request handler
|
|
144
|
+
const onRequest = (request) => {
|
|
145
|
+
// Filter by resource type
|
|
146
|
+
if (!RELEVANT_RESOURCE_TYPES.has(request.resourceType())) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Filter out streaming, websocket, and other real-time requests
|
|
150
|
+
if (['websocket', 'media', 'eventsource', 'manifest', 'other'].includes(request.resourceType())) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Filter out by URL patterns
|
|
154
|
+
const url = request.url().toLowerCase();
|
|
155
|
+
if (IGNORED_URL_PATTERNS.some(pattern => url.includes(pattern))) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Filter out data URLs and blob URLs
|
|
159
|
+
if (url.startsWith('data:') || url.startsWith('blob:')) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
pendingRequests.add(request);
|
|
163
|
+
lastActivity = Date.now();
|
|
164
|
+
this.log(`Request started: ${request.url()}`);
|
|
165
|
+
};
|
|
166
|
+
// Setup response handler
|
|
167
|
+
const onResponse = (response) => {
|
|
168
|
+
const request = response.request();
|
|
169
|
+
if (!pendingRequests.has(request)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Filter by content type if available
|
|
173
|
+
const contentType = response.headers()['content-type'] || '';
|
|
174
|
+
// Skip if content type indicates streaming or real-time data
|
|
175
|
+
if (['streaming', 'video', 'audio', 'webm', 'mp4', 'event-stream', 'websocket', 'protobuf']
|
|
176
|
+
.some(t => contentType.includes(t))) {
|
|
177
|
+
pendingRequests.delete(request);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Only process relevant content types
|
|
181
|
+
if (!RELEVANT_CONTENT_TYPES.some(ct => contentType.includes(ct))) {
|
|
182
|
+
pendingRequests.delete(request);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Skip if response is too large (likely not essential for page load)
|
|
186
|
+
const contentLength = parseInt(response.headers()['content-length'] || '0', 10);
|
|
187
|
+
if (contentLength > 5 * 1024 * 1024) { // 5MB
|
|
188
|
+
pendingRequests.delete(request);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
pendingRequests.delete(request);
|
|
192
|
+
lastActivity = Date.now();
|
|
193
|
+
this.log(`Request resolved: ${request.url()}`);
|
|
194
|
+
};
|
|
195
|
+
// Add event listeners
|
|
196
|
+
this.page.on('request', onRequest);
|
|
197
|
+
this.page.on('response', onResponse);
|
|
198
|
+
try {
|
|
199
|
+
// Wait for idle time
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
while (true) {
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Sleep 100ms
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
if (pendingRequests.size === 0 &&
|
|
205
|
+
(now - lastActivity) >= this.options.waitForNetworkIdleTime) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (now - startTime > maxWaitTime) {
|
|
209
|
+
this.log(`Network timeout after ${maxWaitTime}ms with ${pendingRequests.size} pending requests`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
// Clean up event listeners
|
|
216
|
+
this.page.removeListener('request', onRequest);
|
|
217
|
+
this.page.removeListener('response', onResponse);
|
|
218
|
+
}
|
|
219
|
+
const totalTime = (Date.now() - start) / 1000;
|
|
220
|
+
this.log(`Network stabilized in ${totalTime.toFixed(2)}s`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Wait for the page to become visually stable
|
|
224
|
+
* @param timeout Maximum time to wait for stability in ms
|
|
225
|
+
*/
|
|
226
|
+
async waitForVisualStability(timeout = DEFAULT_PAGE_STABILITY_TIMEOUT) {
|
|
227
|
+
const start = Date.now();
|
|
228
|
+
this.log("Checking visual stability");
|
|
229
|
+
try {
|
|
230
|
+
let lastScreenshot = await this.page.screenshot();
|
|
231
|
+
let stabilityCount = 0;
|
|
232
|
+
const deadline = start + timeout;
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
this.log(`Waiting for ${this.options.checkInterval}`);
|
|
235
|
+
await this.page.waitForTimeout(this.options.checkInterval);
|
|
236
|
+
this.log(`Done waiting`);
|
|
237
|
+
try {
|
|
238
|
+
this.log("Taking screenshot...");
|
|
239
|
+
const currentScreenshot = await this.page.screenshot();
|
|
240
|
+
this.log("Comparing screenshots...");
|
|
241
|
+
const diffResult = await this.compareScreenshots(lastScreenshot, currentScreenshot);
|
|
242
|
+
if (diffResult.error) {
|
|
243
|
+
this.log(`Comparison error: ${diffResult.error}`);
|
|
244
|
+
stabilityCount = 0;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.log(`Screenshot difference: ${diffResult.difference.toFixed(4)}`);
|
|
248
|
+
if (diffResult.difference < this.options.differenceThreshold) {
|
|
249
|
+
stabilityCount++;
|
|
250
|
+
if (stabilityCount >= this.options.requiredStableChecks) {
|
|
251
|
+
this.log(`Visual stability achieved (difference: ${diffResult.difference.toFixed(4)})`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
stabilityCount = 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
lastScreenshot = currentScreenshot;
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
this.log(`Screenshot/comparison error: ${e instanceof Error ? e.message : String(e)}`);
|
|
263
|
+
stabilityCount = 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// If we exit the loop without returning, we timed out
|
|
267
|
+
this.log("Visual stability check timed out");
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
this.log(`Visual stability check error: ${e instanceof Error ? e.message : String(e)}`);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
const totalTime = (Date.now() - start) / 1000;
|
|
274
|
+
this.log(`Visual stability check took ${totalTime.toFixed(2)}s`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Wait for both network and visual stability
|
|
279
|
+
* @param timeout Maximum time to wait for page load
|
|
280
|
+
*/
|
|
281
|
+
async waitForStability(timeout) {
|
|
282
|
+
const maxWaitTime = timeout ?? this.options.maximumWaitPageLoadTime;
|
|
283
|
+
const startTime = Date.now();
|
|
284
|
+
this.lastStart = startTime;
|
|
285
|
+
// Track our deadlines
|
|
286
|
+
const minWaitDeadline = startTime + this.options.minimumWaitPageLoadTime;
|
|
287
|
+
const maxWaitDeadline = startTime + maxWaitTime;
|
|
288
|
+
this.log(`Starting stability wait (min: ${this.options.minimumWaitPageLoadTime}ms, max: ${maxWaitTime}ms)`);
|
|
289
|
+
try {
|
|
290
|
+
// Wait for network stability, but don't exceed max wait time
|
|
291
|
+
const remainingForNetwork = Math.max(0, maxWaitDeadline - Date.now());
|
|
292
|
+
if (remainingForNetwork > 0) {
|
|
293
|
+
await this.waitForNetworkStability(remainingForNetwork);
|
|
294
|
+
}
|
|
295
|
+
// Wait for visual stability with whatever time remains, but don't exceed max wait time
|
|
296
|
+
if (!this.options.disableVisualStability) {
|
|
297
|
+
const remainingForVisual = Math.max(0, maxWaitDeadline - Date.now());
|
|
298
|
+
if (remainingForVisual > 0) {
|
|
299
|
+
await this.waitForVisualStability(remainingForVisual);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Ensure we've waited at least the minimum time
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
if (now < minWaitDeadline) {
|
|
305
|
+
const remainingMinWait = minWaitDeadline - now;
|
|
306
|
+
this.log(`Waiting additional ${remainingMinWait}ms to meet minimum wait time`);
|
|
307
|
+
await new Promise(resolve => setTimeout(resolve, remainingMinWait));
|
|
308
|
+
}
|
|
309
|
+
const totalTime = Date.now() - startTime;
|
|
310
|
+
this.log(`Page stability wait completed in ${totalTime}ms`);
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
this.log(`Error during stability wait: ${e instanceof Error ? e.message : String(e)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
import { BrowserContext, Page } from "playwright";
|
|
3
|
+
export interface TabEvents {
|
|
4
|
+
'tabChanged': (page: Page) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface TabState {
|
|
7
|
+
activeTab: number;
|
|
8
|
+
tabs: {
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
export interface TabManagerOptions {
|
|
14
|
+
switchOnActivity?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class TabManager {
|
|
17
|
+
private context;
|
|
18
|
+
private activePage;
|
|
19
|
+
private options;
|
|
20
|
+
private pollInterval?;
|
|
21
|
+
readonly events: EventEmitter<TabEvents>;
|
|
22
|
+
constructor(context: BrowserContext, options?: TabManagerOptions);
|
|
23
|
+
initialize(): Promise<void>;
|
|
24
|
+
private initializeExistingPages;
|
|
25
|
+
private onPageCreated;
|
|
26
|
+
private setupPageTracking;
|
|
27
|
+
private startActivityPolling;
|
|
28
|
+
setActivePage(page: Page): void;
|
|
29
|
+
switchTab(index: number): Promise<void>;
|
|
30
|
+
getActivePage(): Page;
|
|
31
|
+
getPages(): Page[];
|
|
32
|
+
retrieveState(): Promise<TabState>;
|
|
33
|
+
debugVisibilityStates(): Promise<({
|
|
34
|
+
hasFocus: boolean;
|
|
35
|
+
hidden: boolean;
|
|
36
|
+
visibilityState: DocumentVisibilityState;
|
|
37
|
+
index: number;
|
|
38
|
+
url: string;
|
|
39
|
+
error?: undefined;
|
|
40
|
+
} | {
|
|
41
|
+
index: number;
|
|
42
|
+
url: string;
|
|
43
|
+
error: boolean;
|
|
44
|
+
})[]>;
|
|
45
|
+
debugFocusStates(): Promise<({
|
|
46
|
+
hasFocus: boolean;
|
|
47
|
+
isVisible: boolean;
|
|
48
|
+
isHidden: boolean;
|
|
49
|
+
url: string;
|
|
50
|
+
index: number;
|
|
51
|
+
error?: undefined;
|
|
52
|
+
} | {
|
|
53
|
+
index: number;
|
|
54
|
+
url: string;
|
|
55
|
+
error: boolean;
|
|
56
|
+
})[]>;
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
package/dist/web/tabs.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Keeps track of current tab, enables describing open tabs, and switching between open tabs
|
|
2
|
+
import logger from "@/logger";
|
|
3
|
+
import EventEmitter from "eventemitter3";
|
|
4
|
+
import { retryOnErrorIsSuccess } from "@/common";
|
|
5
|
+
export class TabManager {
|
|
6
|
+
context;
|
|
7
|
+
activePage;
|
|
8
|
+
options;
|
|
9
|
+
pollInterval;
|
|
10
|
+
events;
|
|
11
|
+
constructor(context, options = {}) {
|
|
12
|
+
this.context = context;
|
|
13
|
+
this.options = {
|
|
14
|
+
switchOnActivity: true,
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
this.events = new EventEmitter();
|
|
18
|
+
// Track new pages
|
|
19
|
+
this.context.on('page', this.onPageCreated.bind(this));
|
|
20
|
+
}
|
|
21
|
+
async initialize() {
|
|
22
|
+
// Initialize existing pages and tracking
|
|
23
|
+
await this.initializeExistingPages();
|
|
24
|
+
}
|
|
25
|
+
async initializeExistingPages() {
|
|
26
|
+
const pages = this.context.pages();
|
|
27
|
+
logger.debug(`initializeExistingPages: found ${pages.length} existing pages`);
|
|
28
|
+
// Set up existing pages
|
|
29
|
+
for (const page of pages) {
|
|
30
|
+
logger.debug(`Setting up page: ${page.url()}`);
|
|
31
|
+
await this.onPageCreated(page);
|
|
32
|
+
}
|
|
33
|
+
// Start polling for activity if enabled
|
|
34
|
+
if (this.options.switchOnActivity) {
|
|
35
|
+
logger.debug('Starting activity polling');
|
|
36
|
+
this.startActivityPolling();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async onPageCreated(page) {
|
|
40
|
+
logger.debug(`onPageCreated called for: ${page.url()}`);
|
|
41
|
+
// If this is the only page or no active page set, make it active
|
|
42
|
+
if (!this.activePage || this.context.pages().length === 1) {
|
|
43
|
+
this.setActivePage(page);
|
|
44
|
+
}
|
|
45
|
+
// Only set up tracking if switchOnActivity is enabled
|
|
46
|
+
if (this.options.switchOnActivity) {
|
|
47
|
+
// Set up activity tracking immediately
|
|
48
|
+
await this.setupPageTracking(page);
|
|
49
|
+
// Re-inject tracking after navigation
|
|
50
|
+
page.on('load', async () => {
|
|
51
|
+
await this.setupPageTracking(page);
|
|
52
|
+
// If this is the active page and it navigated from about:blank, re-emit tabChanged
|
|
53
|
+
if (this.activePage === page && page.url() !== 'about:blank') {
|
|
54
|
+
logger.debug(`Re-emitting tabChanged after navigation to: ${page.url()}`);
|
|
55
|
+
this.events.emit('tabChanged', page);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Also inject on DOMContentLoaded for faster setup
|
|
59
|
+
page.on('domcontentloaded', async () => {
|
|
60
|
+
await this.setupPageTracking(page);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Don't automatically switch to new tabs - let the user decide
|
|
64
|
+
// This prevents unwanted switches when links open in new tabs
|
|
65
|
+
// Clean up when page closes
|
|
66
|
+
page.on('close', () => {
|
|
67
|
+
if (this.activePage === page) {
|
|
68
|
+
const pages = this.context.pages();
|
|
69
|
+
if (pages.length > 0) {
|
|
70
|
+
this.setActivePage(pages[0]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Start polling if this is the first page and switchOnActivity is enabled
|
|
75
|
+
if (this.context.pages().length === 1 && this.options.switchOnActivity && !this.pollInterval) {
|
|
76
|
+
logger.debug('Starting activity polling from onPageCreated (first page)');
|
|
77
|
+
this.startActivityPolling();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async setupPageTracking(page) {
|
|
81
|
+
// Inject the tracking script with retries
|
|
82
|
+
await retryOnErrorIsSuccess(async () => {
|
|
83
|
+
const currentUrl = page.url();
|
|
84
|
+
logger.debug(`Setting up tracking for: ${currentUrl}`);
|
|
85
|
+
// Inject activity tracking
|
|
86
|
+
await page.evaluate(() => {
|
|
87
|
+
// Initialize activity tracking storage
|
|
88
|
+
if (!window.__tabActivityTime) {
|
|
89
|
+
window.__tabActivityTime = 0;
|
|
90
|
+
}
|
|
91
|
+
// Set initial activity time to NOW since we're setting up tracking
|
|
92
|
+
// This ensures the initial page is considered active
|
|
93
|
+
window.__tabActivityTime = Date.now();
|
|
94
|
+
// Track any user interaction
|
|
95
|
+
const recordActivity = () => {
|
|
96
|
+
window.__tabActivityTime = Date.now();
|
|
97
|
+
};
|
|
98
|
+
// Remove any existing listeners to avoid duplicates
|
|
99
|
+
const listeners = window.__tabListeners;
|
|
100
|
+
if (listeners) {
|
|
101
|
+
listeners.forEach((l) => {
|
|
102
|
+
document.removeEventListener(l.event, l.handler, true);
|
|
103
|
+
window.removeEventListener(l.event, l.handler, true);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Store references to our listeners
|
|
107
|
+
const newListeners = [];
|
|
108
|
+
// Helper to add tracked event listener
|
|
109
|
+
const addTrackedListener = (target, event, handler) => {
|
|
110
|
+
target.addEventListener(event, handler, true);
|
|
111
|
+
newListeners.push({ event, handler });
|
|
112
|
+
};
|
|
113
|
+
// Throttled mouse move handler
|
|
114
|
+
let lastMouseMoveTime = 0;
|
|
115
|
+
const throttledMouseMove = () => {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
if (now - lastMouseMoveTime > 500) {
|
|
118
|
+
lastMouseMoveTime = now;
|
|
119
|
+
recordActivity();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Mouse events
|
|
123
|
+
addTrackedListener(document, 'mousedown', recordActivity);
|
|
124
|
+
addTrackedListener(document, 'click', recordActivity);
|
|
125
|
+
addTrackedListener(document, 'mousemove', throttledMouseMove);
|
|
126
|
+
addTrackedListener(document, 'mouseenter', recordActivity);
|
|
127
|
+
// Keyboard events
|
|
128
|
+
addTrackedListener(document, 'keydown', recordActivity);
|
|
129
|
+
// Focus events
|
|
130
|
+
addTrackedListener(window, 'focus', recordActivity);
|
|
131
|
+
// Scroll events
|
|
132
|
+
let lastScrollTime = 0;
|
|
133
|
+
const throttledScroll = () => {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
if (now - lastScrollTime > 500) {
|
|
136
|
+
lastScrollTime = now;
|
|
137
|
+
recordActivity();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
addTrackedListener(document, 'scroll', throttledScroll);
|
|
141
|
+
// Visibility change
|
|
142
|
+
addTrackedListener(document, 'visibilitychange', () => {
|
|
143
|
+
if (!document.hidden) {
|
|
144
|
+
recordActivity();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// Store listeners for cleanup
|
|
148
|
+
window.__tabListeners = newListeners;
|
|
149
|
+
});
|
|
150
|
+
}, { mode: 'retry_all', delayMs: 200, retryLimit: 5 });
|
|
151
|
+
}
|
|
152
|
+
startActivityPolling() {
|
|
153
|
+
// Poll for activity every 200ms
|
|
154
|
+
this.pollInterval = setInterval(async () => {
|
|
155
|
+
const pages = this.context.pages();
|
|
156
|
+
let mostRecentPage = null;
|
|
157
|
+
let mostRecentTime = 0;
|
|
158
|
+
// Find the page with the most recent activity
|
|
159
|
+
for (const page of pages) {
|
|
160
|
+
if (page.isClosed())
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
const lastActivity = await page.evaluate(() => {
|
|
164
|
+
// Initialize if it doesn't exist (defensive programming)
|
|
165
|
+
if (typeof window.__tabActivityTime === 'undefined') {
|
|
166
|
+
window.__tabActivityTime = 0;
|
|
167
|
+
// If tracking wasn't set up, return -1 as a signal
|
|
168
|
+
if (!window.__tabListeners) {
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return window.__tabActivityTime;
|
|
173
|
+
});
|
|
174
|
+
// If tracking isn't set up (-1), inject it now
|
|
175
|
+
if (lastActivity === -1) {
|
|
176
|
+
logger.debug(`Tracking not set up for ${page.url()}, injecting now...`);
|
|
177
|
+
await this.setupPageTracking(page);
|
|
178
|
+
continue; // Skip this page for this polling cycle
|
|
179
|
+
}
|
|
180
|
+
if (lastActivity > mostRecentTime) {
|
|
181
|
+
mostRecentTime = lastActivity;
|
|
182
|
+
mostRecentPage = page;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
// Page might be navigating
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Switch to the most recently active page if it's different and activity is recent
|
|
190
|
+
if (mostRecentPage &&
|
|
191
|
+
mostRecentPage !== this.activePage &&
|
|
192
|
+
mostRecentTime > Date.now() - 1000) { // Activity within last second
|
|
193
|
+
logger.trace(`Activity detected on ${mostRecentPage.url()}, switching...`);
|
|
194
|
+
this.setActivePage(mostRecentPage);
|
|
195
|
+
}
|
|
196
|
+
}, 200);
|
|
197
|
+
}
|
|
198
|
+
setActivePage(page) {
|
|
199
|
+
const isInitialPage = !this.activePage;
|
|
200
|
+
if (this.activePage === page && !isInitialPage) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
logger.debug(`Active tab changed to: ${page.url()}`);
|
|
204
|
+
this.activePage = page;
|
|
205
|
+
this.events.emit('tabChanged', page);
|
|
206
|
+
page.removeAllListeners('framenavigated');
|
|
207
|
+
page.on('framenavigated', async (frame) => {
|
|
208
|
+
if (frame === page.mainFrame()) {
|
|
209
|
+
// const url = frame.url();
|
|
210
|
+
// await logNavigation(url);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async switchTab(index) {
|
|
215
|
+
const pages = this.context.pages();
|
|
216
|
+
if (index < 0 || index >= pages.length) {
|
|
217
|
+
throw new Error(`Invalid tab index: ${index}`);
|
|
218
|
+
}
|
|
219
|
+
const page = pages[index];
|
|
220
|
+
logger.debug(`Switching to tab ${index} (${page.url()})`);
|
|
221
|
+
await page.bringToFront();
|
|
222
|
+
this.setActivePage(page);
|
|
223
|
+
}
|
|
224
|
+
getActivePage() {
|
|
225
|
+
return this.activePage;
|
|
226
|
+
}
|
|
227
|
+
getPages() {
|
|
228
|
+
return this.context.pages();
|
|
229
|
+
}
|
|
230
|
+
async retrieveState() {
|
|
231
|
+
let activeIndex = -1;
|
|
232
|
+
let tabs = [];
|
|
233
|
+
for (const [i, page] of this.context.pages().entries()) {
|
|
234
|
+
if (page === this.activePage) {
|
|
235
|
+
activeIndex = i;
|
|
236
|
+
}
|
|
237
|
+
let title;
|
|
238
|
+
try {
|
|
239
|
+
title = await page.title();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
logger.warn('Could not load page title while retrieving tab state');
|
|
243
|
+
title = '(could not load title)';
|
|
244
|
+
}
|
|
245
|
+
const url = page.url();
|
|
246
|
+
tabs.push({ title, url });
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
activeTab: activeIndex,
|
|
250
|
+
tabs: tabs
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async debugVisibilityStates() {
|
|
254
|
+
const states = await Promise.all(this.context.pages().map(async (page, i) => {
|
|
255
|
+
try {
|
|
256
|
+
const hasFocus = await page.evaluate(() => ({
|
|
257
|
+
hasFocus: document.hasFocus(),
|
|
258
|
+
hidden: document.hidden,
|
|
259
|
+
visibilityState: document.visibilityState
|
|
260
|
+
}));
|
|
261
|
+
return { index: i, url: page.url(), ...hasFocus };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { index: i, url: page.url(), error: true };
|
|
265
|
+
}
|
|
266
|
+
}));
|
|
267
|
+
return states;
|
|
268
|
+
}
|
|
269
|
+
async debugFocusStates() {
|
|
270
|
+
const states = await Promise.all(this.context.pages().map(async (page, i) => {
|
|
271
|
+
try {
|
|
272
|
+
const state = await page.evaluate(() => ({
|
|
273
|
+
hasFocus: document.hasFocus(),
|
|
274
|
+
isVisible: document.visibilityState === 'visible',
|
|
275
|
+
isHidden: document.hidden,
|
|
276
|
+
url: window.location.href
|
|
277
|
+
}));
|
|
278
|
+
return { index: i, ...state };
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return { index: i, url: page.url(), error: true };
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
console.log('Focus states:', states);
|
|
285
|
+
return states;
|
|
286
|
+
}
|
|
287
|
+
// Clean up
|
|
288
|
+
destroy() {
|
|
289
|
+
if (this.pollInterval) {
|
|
290
|
+
clearInterval(this.pollInterval);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|