@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.
Files changed (142) hide show
  1. package/dist/actions/desktopActions.d.ts +58 -0
  2. package/dist/actions/desktopActions.js +154 -0
  3. package/dist/actions/index.d.ts +26 -0
  4. package/dist/actions/index.js +11 -0
  5. package/dist/actions/taskActions.d.ts +8 -0
  6. package/dist/actions/taskActions.js +27 -0
  7. package/dist/actions/types.d.ts +31 -0
  8. package/dist/actions/types.js +5 -0
  9. package/dist/actions/util.d.ts +20 -0
  10. package/dist/actions/util.js +297 -0
  11. package/dist/actions/webActions.d.ts +79 -0
  12. package/dist/actions/webActions.js +190 -0
  13. package/dist/agent/browserAgent.d.ts +29 -0
  14. package/dist/agent/browserAgent.js +122 -0
  15. package/dist/agent/errors.d.ts +8 -0
  16. package/dist/agent/errors.js +68 -0
  17. package/dist/agent/index.d.ts +53 -0
  18. package/dist/agent/index.js +377 -0
  19. package/dist/agent/narrator.d.ts +4 -0
  20. package/dist/agent/narrator.js +63 -0
  21. package/dist/agent/pause.test.d.ts +1 -0
  22. package/dist/agent/pause.test.js +194 -0
  23. package/dist/ai/baml_client/async_client.d.ts +73 -0
  24. package/dist/ai/baml_client/async_client.js +152 -0
  25. package/dist/ai/baml_client/async_request.d.ts +35 -0
  26. package/dist/ai/baml_client/async_request.js +98 -0
  27. package/dist/ai/baml_client/config.d.ts +11 -0
  28. package/dist/ai/baml_client/config.js +20 -0
  29. package/dist/ai/baml_client/globals.d.ts +16 -0
  30. package/dist/ai/baml_client/globals.js +37 -0
  31. package/dist/ai/baml_client/index.d.ts +16 -0
  32. package/dist/ai/baml_client/index.js +41 -0
  33. package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
  34. package/dist/ai/baml_client/inlinedbaml.js +31 -0
  35. package/dist/ai/baml_client/parser.d.ts +53 -0
  36. package/dist/ai/baml_client/parser.js +86 -0
  37. package/dist/ai/baml_client/partial_types.d.ts +61 -0
  38. package/dist/ai/baml_client/partial_types.js +10 -0
  39. package/dist/ai/baml_client/sync_client.d.ts +50 -0
  40. package/dist/ai/baml_client/sync_client.js +100 -0
  41. package/dist/ai/baml_client/sync_request.d.ts +35 -0
  42. package/dist/ai/baml_client/sync_request.js +98 -0
  43. package/dist/ai/baml_client/tracing.d.ts +16 -0
  44. package/dist/ai/baml_client/tracing.js +18 -0
  45. package/dist/ai/baml_client/type_builder.d.ts +38 -0
  46. package/dist/ai/baml_client/type_builder.js +96 -0
  47. package/dist/ai/baml_client/types.d.ts +69 -0
  48. package/dist/ai/baml_client/types.js +15 -0
  49. package/dist/ai/claudeCode.d.ts +1 -0
  50. package/dist/ai/claudeCode.js +186 -0
  51. package/dist/ai/modelHarness.d.ts +39 -0
  52. package/dist/ai/modelHarness.js +189 -0
  53. package/dist/ai/multiModelHarness.d.ts +27 -0
  54. package/dist/ai/multiModelHarness.js +52 -0
  55. package/dist/ai/types.d.ts +90 -0
  56. package/dist/ai/types.js +5 -0
  57. package/dist/ai/util.d.ts +13 -0
  58. package/dist/ai/util.js +165 -0
  59. package/dist/cli.d.ts +0 -0
  60. package/dist/cli.js +3 -0
  61. package/dist/common/actions.d.ts +0 -0
  62. package/dist/common/actions.js +14 -0
  63. package/dist/common/events.d.ts +15 -0
  64. package/dist/common/events.js +3 -0
  65. package/dist/common/failure.d.ts +68 -0
  66. package/dist/common/failure.js +1 -0
  67. package/dist/common/index.d.ts +3 -0
  68. package/dist/common/index.js +3 -0
  69. package/dist/common/retry.d.ts +16 -0
  70. package/dist/common/retry.js +30 -0
  71. package/dist/common/util.d.ts +23 -0
  72. package/dist/common/util.js +132 -0
  73. package/dist/connectors/browserConnector.d.ts +40 -0
  74. package/dist/connectors/browserConnector.js +127 -0
  75. package/dist/connectors/desktopConnector.d.ts +58 -0
  76. package/dist/connectors/desktopConnector.js +68 -0
  77. package/dist/connectors/index.d.ts +10 -0
  78. package/dist/connectors/index.js +2 -0
  79. package/dist/index.cjs +6143 -0
  80. package/dist/index.d.cts +825 -0
  81. package/dist/index.d.ts +20 -0
  82. package/dist/index.js +26 -0
  83. package/dist/index.mjs +6097 -0
  84. package/dist/logger.d.ts +3 -0
  85. package/dist/logger.js +15 -0
  86. package/dist/memory/agentMemory.d.ts +37 -0
  87. package/dist/memory/agentMemory.js +121 -0
  88. package/dist/memory/image.d.ts +26 -0
  89. package/dist/memory/image.js +106 -0
  90. package/dist/memory/index.d.ts +3 -0
  91. package/dist/memory/index.js +3 -0
  92. package/dist/memory/masking.d.ts +7 -0
  93. package/dist/memory/masking.js +141 -0
  94. package/dist/memory/masking.test.d.ts +1 -0
  95. package/dist/memory/masking.test.js +117 -0
  96. package/dist/memory/observation.d.ts +38 -0
  97. package/dist/memory/observation.js +79 -0
  98. package/dist/memory/rendering/index.d.ts +10 -0
  99. package/dist/memory/rendering/index.js +13 -0
  100. package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
  101. package/dist/memory/rendering/renderJsonParts.js +115 -0
  102. package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
  103. package/dist/memory/rendering/renderJsonParts.test.js +217 -0
  104. package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
  105. package/dist/memory/rendering/renderXmlParts.js +127 -0
  106. package/dist/memory/serde.d.ts +20 -0
  107. package/dist/memory/serde.js +81 -0
  108. package/dist/memory/util.d.ts +2 -0
  109. package/dist/memory/util.js +17 -0
  110. package/dist/telemetry/events.d.ts +9 -0
  111. package/dist/telemetry/events.js +80 -0
  112. package/dist/telemetry/index.d.ts +6 -0
  113. package/dist/telemetry/index.js +91 -0
  114. package/dist/types.d.ts +30 -0
  115. package/dist/types.js +1 -0
  116. package/dist/util.d.ts +1 -0
  117. package/dist/util.js +12 -0
  118. package/dist/version.d.ts +1 -0
  119. package/dist/version.js +3 -0
  120. package/dist/web/browserProvider.d.ts +23 -0
  121. package/dist/web/browserProvider.js +153 -0
  122. package/dist/web/harness.d.ts +102 -0
  123. package/dist/web/harness.js +340 -0
  124. package/dist/web/stability.d.ts +57 -0
  125. package/dist/web/stability.js +316 -0
  126. package/dist/web/tabs.d.ts +58 -0
  127. package/dist/web/tabs.js +293 -0
  128. package/dist/web/transformer.d.ts +7 -0
  129. package/dist/web/transformer.js +41 -0
  130. package/dist/web/types.d.ts +32 -0
  131. package/dist/web/types.js +2 -0
  132. package/dist/web/util.d.ts +3 -0
  133. package/dist/web/util.js +81 -0
  134. package/dist/web/visualizer/cursor.d.ts +16 -0
  135. package/dist/web/visualizer/cursor.js +152 -0
  136. package/dist/web/visualizer/index.d.ts +22 -0
  137. package/dist/web/visualizer/index.js +47 -0
  138. package/dist/web/visualizer/mouseEffects.d.ts +18 -0
  139. package/dist/web/visualizer/mouseEffects.js +287 -0
  140. package/dist/web/visualizer/typeEffects.d.ts +10 -0
  141. package/dist/web/visualizer/typeEffects.js +190 -0
  142. 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
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import type { Page } from 'playwright';
2
+ export declare class DOMTransformer {
3
+ private initializedPages;
4
+ constructor();
5
+ setActivePage(newPage: Page): void;
6
+ setupScriptForPage(targetPage: Page): Promise<void>;
7
+ }