@mastra/stagehand 0.0.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 +93 -0
- package/dist/index.cjs +1408 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +590 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +1397 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { Stagehand } from '@browserbasehq/stagehand';
|
|
2
|
+
import { MastraBrowser, DEFAULT_THREAD_ID, ScreencastStreamImpl, ThreadManager } from '@mastra/core/browser';
|
|
3
|
+
import { createTool } from '@mastra/core/tools';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
// src/stagehand-browser.ts
|
|
7
|
+
var StagehandThreadManager = class extends ThreadManager {
|
|
8
|
+
sharedStagehand = null;
|
|
9
|
+
sessions = /* @__PURE__ */ new Map();
|
|
10
|
+
createStagehand;
|
|
11
|
+
onBrowserCreated;
|
|
12
|
+
/** Map of thread ID to dedicated Stagehand instance (for 'thread' mode) */
|
|
13
|
+
threadStagehands = /* @__PURE__ */ new Map();
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super(config);
|
|
16
|
+
this.createStagehand = config.createStagehand;
|
|
17
|
+
this.onBrowserCreated = config.onBrowserCreated;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set the shared Stagehand instance (called after browser launch).
|
|
21
|
+
*/
|
|
22
|
+
setStagehand(instance) {
|
|
23
|
+
this.sharedStagehand = instance;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Clear the shared Stagehand instance (called when browser disconnects).
|
|
27
|
+
*/
|
|
28
|
+
clearStagehand() {
|
|
29
|
+
this.sharedStagehand = null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Set the factory function for creating new Stagehand instances.
|
|
33
|
+
* Required for 'browser' scope mode.
|
|
34
|
+
*/
|
|
35
|
+
setCreateStagehand(factory) {
|
|
36
|
+
this.createStagehand = factory;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the shared Stagehand instance.
|
|
40
|
+
*/
|
|
41
|
+
getSharedStagehand() {
|
|
42
|
+
if (!this.sharedStagehand) {
|
|
43
|
+
throw new Error("Stagehand not initialized");
|
|
44
|
+
}
|
|
45
|
+
return this.sharedStagehand;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the Stagehand instance for a specific thread.
|
|
49
|
+
* In 'shared' mode, returns the shared instance.
|
|
50
|
+
* In 'thread' mode, returns the thread's dedicated instance.
|
|
51
|
+
*/
|
|
52
|
+
getStagehandForThread(threadId) {
|
|
53
|
+
if (this.scope === "thread") {
|
|
54
|
+
const session = this.sessions.get(threadId);
|
|
55
|
+
return session?.stagehand;
|
|
56
|
+
}
|
|
57
|
+
return this.sharedStagehand ?? void 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the Stagehand page for a thread.
|
|
61
|
+
* Returns the active page from the thread's Stagehand instance.
|
|
62
|
+
*/
|
|
63
|
+
getPageForThread(threadId) {
|
|
64
|
+
const stagehand = this.getStagehandForThread(threadId);
|
|
65
|
+
return stagehand?.context?.activePage();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the shared manager - returns the active page or the Stagehand instance.
|
|
69
|
+
*/
|
|
70
|
+
getSharedManager() {
|
|
71
|
+
const stagehand = this.getSharedStagehand();
|
|
72
|
+
return stagehand.context.activePage() ?? stagehand;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a new session for a thread.
|
|
76
|
+
*/
|
|
77
|
+
async createSession(threadId) {
|
|
78
|
+
const savedState = this.getSavedBrowserState(threadId);
|
|
79
|
+
const session = {
|
|
80
|
+
threadId,
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
browserState: savedState
|
|
83
|
+
};
|
|
84
|
+
if (this.scope === "thread") {
|
|
85
|
+
if (!this.createStagehand) {
|
|
86
|
+
throw new Error("createStagehand factory not set - required for thread scope");
|
|
87
|
+
}
|
|
88
|
+
this.logger?.debug?.(`Creating dedicated Stagehand instance for thread ${threadId}`);
|
|
89
|
+
const stagehand = await this.createStagehand();
|
|
90
|
+
session.stagehand = stagehand;
|
|
91
|
+
this.threadStagehands.set(threadId, stagehand);
|
|
92
|
+
if (savedState && savedState.tabs.length > 0) {
|
|
93
|
+
this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
|
|
94
|
+
await this.restoreBrowserState(stagehand, savedState);
|
|
95
|
+
}
|
|
96
|
+
this.onBrowserCreated?.(stagehand, threadId);
|
|
97
|
+
}
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Restore browser state (multiple tabs) to a Stagehand instance.
|
|
102
|
+
*/
|
|
103
|
+
async restoreBrowserState(stagehand, state) {
|
|
104
|
+
try {
|
|
105
|
+
const context = stagehand.context;
|
|
106
|
+
if (!context) return;
|
|
107
|
+
const firstTab = state.tabs[0];
|
|
108
|
+
if (firstTab?.url) {
|
|
109
|
+
const page = context.activePage();
|
|
110
|
+
if (page) {
|
|
111
|
+
await page.goto(firstTab.url, { waitUntil: "domcontentloaded" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (let i = 1; i < state.tabs.length; i++) {
|
|
115
|
+
const tab = state.tabs[i];
|
|
116
|
+
if (tab?.url) {
|
|
117
|
+
await context.newPage(tab.url);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const pages = context.pages();
|
|
121
|
+
const targetPage = pages[state.activeTabIndex];
|
|
122
|
+
if (targetPage && targetPage !== context.activePage()) {
|
|
123
|
+
context.setActivePage(targetPage);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.logger?.warn?.(`Failed to restore browser state: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Switch to an existing session.
|
|
131
|
+
* For 'thread' mode, no switching needed - each thread has its own instance.
|
|
132
|
+
* For 'shared' mode, nothing to switch.
|
|
133
|
+
*/
|
|
134
|
+
async switchToSession(_session) {
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the manager for a specific session.
|
|
138
|
+
*/
|
|
139
|
+
getManagerForSession(session) {
|
|
140
|
+
if (this.scope === "thread" && session.stagehand) {
|
|
141
|
+
return session.stagehand.context.activePage() ?? session.stagehand;
|
|
142
|
+
}
|
|
143
|
+
return this.getSharedManager();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Destroy a session and clean up resources.
|
|
147
|
+
*/
|
|
148
|
+
async doDestroySession(session) {
|
|
149
|
+
if (this.scope === "thread" && session.stagehand) {
|
|
150
|
+
try {
|
|
151
|
+
await session.stagehand.close();
|
|
152
|
+
this.logger?.debug?.(`Closed Stagehand instance for thread ${session.threadId}`);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger?.warn?.(`Failed to close Stagehand for thread ${session.threadId}: ${error}`);
|
|
155
|
+
}
|
|
156
|
+
this.threadStagehands.delete(session.threadId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Clean up all thread sessions.
|
|
161
|
+
*/
|
|
162
|
+
async destroyAll() {
|
|
163
|
+
for (const [threadId, stagehand] of this.threadStagehands) {
|
|
164
|
+
try {
|
|
165
|
+
await stagehand.close();
|
|
166
|
+
} catch {
|
|
167
|
+
this.logger?.debug?.(`Failed to close Stagehand for thread: ${threadId}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.threadStagehands.clear();
|
|
171
|
+
this.sessions.clear();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if any thread Stagehands are still running.
|
|
175
|
+
*/
|
|
176
|
+
hasActiveThreadStagehands() {
|
|
177
|
+
return this.threadStagehands.size > 0;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Clear all session tracking without closing browsers.
|
|
181
|
+
* Used when browsers have been externally closed and we just need to reset state.
|
|
182
|
+
*/
|
|
183
|
+
clearAllSessions() {
|
|
184
|
+
this.threadStagehands.clear();
|
|
185
|
+
this.sessions.clear();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Clear a specific thread's session without closing the browser.
|
|
189
|
+
* Used when a thread's browser has been externally closed.
|
|
190
|
+
* Preserves the browser state for potential restoration.
|
|
191
|
+
* @param threadId - The thread ID to clear
|
|
192
|
+
*/
|
|
193
|
+
clearSession(threadId) {
|
|
194
|
+
const session = this.sessions.get(threadId);
|
|
195
|
+
if (session?.browserState) {
|
|
196
|
+
this.savedBrowserStates.set(threadId, session.browserState);
|
|
197
|
+
}
|
|
198
|
+
this.threadStagehands.delete(threadId);
|
|
199
|
+
this.sessions.delete(threadId);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var actInputSchema = z.object({
|
|
203
|
+
instruction: z.string().describe('Natural language instruction for the action (e.g., "click the login button")'),
|
|
204
|
+
variables: z.record(z.string(), z.string()).optional().describe("Variables to substitute in the instruction using %variableName% syntax"),
|
|
205
|
+
useVision: z.boolean().optional().describe("Whether to use vision capabilities (default: true)"),
|
|
206
|
+
timeout: z.number().optional().describe("Timeout in milliseconds")
|
|
207
|
+
});
|
|
208
|
+
var extractInputSchema = z.object({
|
|
209
|
+
instruction: z.string().describe("Natural language instruction for what data to extract"),
|
|
210
|
+
schema: z.record(z.string(), z.unknown()).optional().describe("JSON schema defining the expected data structure (optional, will return unstructured if omitted)"),
|
|
211
|
+
timeout: z.number().optional().describe("Timeout in milliseconds")
|
|
212
|
+
});
|
|
213
|
+
var observeInputSchema = z.object({
|
|
214
|
+
instruction: z.string().optional().describe(
|
|
215
|
+
'Natural language instruction for what to find (e.g., "find all buttons"). If omitted, finds all interactive elements.'
|
|
216
|
+
),
|
|
217
|
+
onlyVisible: z.boolean().optional().describe("Only return visible elements (default: true)"),
|
|
218
|
+
timeout: z.number().optional().describe("Timeout in milliseconds")
|
|
219
|
+
});
|
|
220
|
+
var navigateInputSchema = z.object({
|
|
221
|
+
url: z.string().describe("The URL to navigate to"),
|
|
222
|
+
waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("When to consider navigation complete (default: domcontentloaded)")
|
|
223
|
+
});
|
|
224
|
+
var closeInputSchema = z.object({});
|
|
225
|
+
var tabsInputSchema = z.object({
|
|
226
|
+
action: z.enum(["list", "new", "switch", "close"]).describe("Action to perform: list all tabs, open new tab, switch to tab, or close tab"),
|
|
227
|
+
index: z.number().int().min(0).optional().describe(
|
|
228
|
+
"Tab index for switch/close actions (0-based). Required for switch, optional for close (defaults to current)."
|
|
229
|
+
),
|
|
230
|
+
url: z.string().optional().describe('URL to navigate to after opening new tab (optional, for "new" action only)')
|
|
231
|
+
}).superRefine((value, ctx) => {
|
|
232
|
+
if (value.action === "switch" && value.index === void 0) {
|
|
233
|
+
ctx.addIssue({
|
|
234
|
+
code: z.ZodIssueCode.custom,
|
|
235
|
+
path: ["index"],
|
|
236
|
+
message: 'index is required when action is "switch"'
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
var stagehandSchemas = {
|
|
241
|
+
// Core AI
|
|
242
|
+
act: actInputSchema,
|
|
243
|
+
extract: extractInputSchema,
|
|
244
|
+
observe: observeInputSchema,
|
|
245
|
+
// Navigation & State
|
|
246
|
+
navigate: navigateInputSchema,
|
|
247
|
+
tabs: tabsInputSchema,
|
|
248
|
+
close: closeInputSchema
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/tools/constants.ts
|
|
252
|
+
var STAGEHAND_TOOLS = {
|
|
253
|
+
// Core AI
|
|
254
|
+
ACT: "stagehand_act",
|
|
255
|
+
EXTRACT: "stagehand_extract",
|
|
256
|
+
OBSERVE: "stagehand_observe",
|
|
257
|
+
// Navigation & State
|
|
258
|
+
NAVIGATE: "stagehand_navigate",
|
|
259
|
+
TABS: "stagehand_tabs",
|
|
260
|
+
CLOSE: "stagehand_close"
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/tools/act.ts
|
|
264
|
+
function createActTool(browser) {
|
|
265
|
+
return createTool({
|
|
266
|
+
id: STAGEHAND_TOOLS.ACT,
|
|
267
|
+
description: 'Perform an action on the page using natural language. Examples: "click the login button", "type hello into the search box", "scroll down".',
|
|
268
|
+
inputSchema: actInputSchema,
|
|
269
|
+
execute: async (input, { agent }) => {
|
|
270
|
+
const threadId = agent?.threadId;
|
|
271
|
+
browser.setCurrentThread(threadId);
|
|
272
|
+
await browser.ensureReady();
|
|
273
|
+
return await browser.act(input, threadId);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function createCloseTool(browser) {
|
|
278
|
+
return createTool({
|
|
279
|
+
id: STAGEHAND_TOOLS.CLOSE,
|
|
280
|
+
description: "Close the browser. Only use when done with all browsing.",
|
|
281
|
+
inputSchema: closeInputSchema,
|
|
282
|
+
execute: async (_input, { agent }) => {
|
|
283
|
+
const threadId = agent?.threadId;
|
|
284
|
+
if (browser.getScope() !== "shared") {
|
|
285
|
+
if (!threadId) {
|
|
286
|
+
throw new Error("stagehand_close requires agent.threadId when browser scope is not shared");
|
|
287
|
+
}
|
|
288
|
+
await browser.closeThreadSession(threadId);
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
hint: "Thread's browser session closed. A new session will be created on next use."
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
await browser.close();
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
hint: "Browser closed. It will be re-launched automatically on next use."
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function createExtractTool(browser) {
|
|
303
|
+
return createTool({
|
|
304
|
+
id: STAGEHAND_TOOLS.EXTRACT,
|
|
305
|
+
description: "Extract structured data from the page using natural language. Can optionally provide a JSON schema for the expected data structure.",
|
|
306
|
+
inputSchema: extractInputSchema,
|
|
307
|
+
execute: async (input, { agent }) => {
|
|
308
|
+
const threadId = agent?.threadId;
|
|
309
|
+
browser.setCurrentThread(threadId);
|
|
310
|
+
await browser.ensureReady();
|
|
311
|
+
return await browser.extract(input, threadId);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function createNavigateTool(browser) {
|
|
316
|
+
return createTool({
|
|
317
|
+
id: STAGEHAND_TOOLS.NAVIGATE,
|
|
318
|
+
description: "Navigate the browser to a URL.",
|
|
319
|
+
inputSchema: navigateInputSchema,
|
|
320
|
+
execute: async (input, { agent }) => {
|
|
321
|
+
const threadId = agent?.threadId;
|
|
322
|
+
browser.setCurrentThread(threadId);
|
|
323
|
+
await browser.ensureReady();
|
|
324
|
+
return await browser.navigate(input, threadId);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function createObserveTool(browser) {
|
|
329
|
+
return createTool({
|
|
330
|
+
id: STAGEHAND_TOOLS.OBSERVE,
|
|
331
|
+
description: "Discover actionable elements on the page. Returns a list of actions that can be performed. Use this to understand what's on the page before acting.",
|
|
332
|
+
inputSchema: observeInputSchema,
|
|
333
|
+
execute: async (input, { agent }) => {
|
|
334
|
+
const threadId = agent?.threadId;
|
|
335
|
+
browser.setCurrentThread(threadId);
|
|
336
|
+
await browser.ensureReady();
|
|
337
|
+
return await browser.observe(input, threadId);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function createTabsTool(browser) {
|
|
342
|
+
return createTool({
|
|
343
|
+
id: STAGEHAND_TOOLS.TABS,
|
|
344
|
+
description: 'Manage browser tabs. Actions: "list" shows all tabs, "new" opens a tab (optionally with URL), "switch" changes to tab by index, "close" closes a tab.',
|
|
345
|
+
inputSchema: tabsInputSchema,
|
|
346
|
+
execute: async (input, { agent }) => {
|
|
347
|
+
const threadId = agent?.threadId;
|
|
348
|
+
browser.setCurrentThread(threadId);
|
|
349
|
+
await browser.ensureReady();
|
|
350
|
+
return await browser.tabs(input, threadId);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/tools/index.ts
|
|
356
|
+
function createStagehandTools(browser) {
|
|
357
|
+
return {
|
|
358
|
+
// Core AI
|
|
359
|
+
[STAGEHAND_TOOLS.ACT]: createActTool(browser),
|
|
360
|
+
[STAGEHAND_TOOLS.EXTRACT]: createExtractTool(browser),
|
|
361
|
+
[STAGEHAND_TOOLS.OBSERVE]: createObserveTool(browser),
|
|
362
|
+
// Navigation & State
|
|
363
|
+
[STAGEHAND_TOOLS.NAVIGATE]: createNavigateTool(browser),
|
|
364
|
+
[STAGEHAND_TOOLS.TABS]: createTabsTool(browser),
|
|
365
|
+
[STAGEHAND_TOOLS.CLOSE]: createCloseTool(browser)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/stagehand-browser.ts
|
|
370
|
+
var StagehandBrowser = class _StagehandBrowser extends MastraBrowser {
|
|
371
|
+
id;
|
|
372
|
+
name = "StagehandBrowser";
|
|
373
|
+
provider = "browserbase/stagehand";
|
|
374
|
+
stagehand = null;
|
|
375
|
+
stagehandConfig;
|
|
376
|
+
/** Active screencast streams per thread (for reconnection on tab changes) */
|
|
377
|
+
activeScreencastStreams = /* @__PURE__ */ new Map();
|
|
378
|
+
/** Debounce timers per thread for tab change reconnection */
|
|
379
|
+
tabChangeDebounceTimers = /* @__PURE__ */ new Map();
|
|
380
|
+
/** Default key for shared scope */
|
|
381
|
+
static SHARED_STREAM_KEY = "__shared__";
|
|
382
|
+
constructor(config = {}) {
|
|
383
|
+
super(config);
|
|
384
|
+
this.id = `stagehand-${Date.now()}`;
|
|
385
|
+
this.stagehandConfig = config;
|
|
386
|
+
let effectiveScope = config.scope ?? "thread";
|
|
387
|
+
if (config.cdpUrl && effectiveScope === "thread") {
|
|
388
|
+
this.logger.warn?.(
|
|
389
|
+
'Browser scope "thread" is not supported when connecting via cdpUrl. Falling back to "shared" (shared browser connection).'
|
|
390
|
+
);
|
|
391
|
+
effectiveScope = "shared";
|
|
392
|
+
}
|
|
393
|
+
this.threadManager = new StagehandThreadManager({
|
|
394
|
+
scope: effectiveScope,
|
|
395
|
+
logger: this.logger,
|
|
396
|
+
// When a new thread session is created, notify listeners so screencast can start
|
|
397
|
+
onSessionCreated: (session) => {
|
|
398
|
+
this.notifyBrowserReady(session.threadId);
|
|
399
|
+
},
|
|
400
|
+
// When a new browser is created for a thread, set up close listener
|
|
401
|
+
onBrowserCreated: (stagehand, threadId) => {
|
|
402
|
+
this.setupCloseListenerForThread(stagehand, threadId);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Close a specific thread's browser session.
|
|
408
|
+
* For 'thread' scope, this closes only that thread's Stagehand instance.
|
|
409
|
+
* For 'shared' scope, this is a no-op (use close() to close the shared browser).
|
|
410
|
+
*/
|
|
411
|
+
async closeThreadSession(threadId) {
|
|
412
|
+
await this.threadManager.destroySession(threadId);
|
|
413
|
+
this.notifyBrowserClosed(threadId);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Ensure browser is ready and thread session exists.
|
|
417
|
+
* For 'thread' scope, this creates a dedicated Stagehand instance for the thread.
|
|
418
|
+
*/
|
|
419
|
+
async ensureReady() {
|
|
420
|
+
this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
|
|
421
|
+
await super.ensureReady();
|
|
422
|
+
const scope = this.getScope();
|
|
423
|
+
const threadId = this.getCurrentThread();
|
|
424
|
+
if (scope === "thread" && threadId && threadId !== DEFAULT_THREAD_ID) {
|
|
425
|
+
await this.getStagehandForThread(threadId);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Lifecycle
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
/**
|
|
432
|
+
* Build Stagehand options from config.
|
|
433
|
+
* Returns the configuration object expected by Stagehand constructor.
|
|
434
|
+
*/
|
|
435
|
+
async buildStagehandOptions() {
|
|
436
|
+
const config = this.stagehandConfig;
|
|
437
|
+
const stagehandOptions = {
|
|
438
|
+
env: config.env ?? "LOCAL",
|
|
439
|
+
// v3 uses "provider/model" format
|
|
440
|
+
model: typeof config.model === "string" ? config.model : config.model?.modelName,
|
|
441
|
+
selfHeal: config.selfHeal ?? true,
|
|
442
|
+
domSettleTimeoutMs: config.domSettleTimeout,
|
|
443
|
+
verbose: config.verbose ?? 1,
|
|
444
|
+
systemPrompt: config.systemPrompt
|
|
445
|
+
};
|
|
446
|
+
if (config.env === "BROWSERBASE") {
|
|
447
|
+
if (config.apiKey) {
|
|
448
|
+
stagehandOptions.apiKey = config.apiKey;
|
|
449
|
+
}
|
|
450
|
+
if (config.projectId) {
|
|
451
|
+
stagehandOptions.projectId = config.projectId;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (config.cdpUrl && config.env !== "BROWSERBASE") {
|
|
455
|
+
const resolvedUrl = await this.resolveCdpUrl(config.cdpUrl);
|
|
456
|
+
const wsUrl = await this.resolveWebSocketUrl(resolvedUrl);
|
|
457
|
+
stagehandOptions.localBrowserLaunchOptions = {
|
|
458
|
+
cdpUrl: wsUrl,
|
|
459
|
+
headless: config.headless,
|
|
460
|
+
viewport: config.viewport
|
|
461
|
+
};
|
|
462
|
+
} else if (config.env !== "BROWSERBASE") {
|
|
463
|
+
stagehandOptions.localBrowserLaunchOptions = {
|
|
464
|
+
headless: config.headless,
|
|
465
|
+
viewport: config.viewport
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return stagehandOptions;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Create a new Stagehand instance with the current config.
|
|
472
|
+
* Used by thread manager for 'browser' isolation mode.
|
|
473
|
+
*/
|
|
474
|
+
async createStagehandInstance() {
|
|
475
|
+
const stagehandOptions = await this.buildStagehandOptions();
|
|
476
|
+
const stagehand = new Stagehand(stagehandOptions);
|
|
477
|
+
await stagehand.init();
|
|
478
|
+
return stagehand;
|
|
479
|
+
}
|
|
480
|
+
async doLaunch() {
|
|
481
|
+
const scope = this.getScope();
|
|
482
|
+
this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
|
|
483
|
+
if (scope === "thread") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.stagehand = await this.createStagehandInstance();
|
|
487
|
+
this.threadManager.setStagehand(this.stagehand);
|
|
488
|
+
this.setupCloseListener(this.stagehand);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Set up close event listener for a Stagehand instance.
|
|
492
|
+
* Listens to both context and page close events for robust detection.
|
|
493
|
+
*/
|
|
494
|
+
setupCloseListener(stagehand) {
|
|
495
|
+
let disconnectHandled = false;
|
|
496
|
+
const handleDisconnect = () => {
|
|
497
|
+
if (disconnectHandled) return;
|
|
498
|
+
disconnectHandled = true;
|
|
499
|
+
this.handleBrowserDisconnected();
|
|
500
|
+
};
|
|
501
|
+
try {
|
|
502
|
+
const context = stagehand.context;
|
|
503
|
+
if (!context) return;
|
|
504
|
+
const contextWithEvents = context;
|
|
505
|
+
if (contextWithEvents?.on) {
|
|
506
|
+
contextWithEvents.on("close", handleDisconnect);
|
|
507
|
+
}
|
|
508
|
+
const pages = context.pages?.() ?? [];
|
|
509
|
+
for (const page of pages) {
|
|
510
|
+
const pageWithEvents = page;
|
|
511
|
+
if (pageWithEvents?.on) {
|
|
512
|
+
pageWithEvents.on("close", () => {
|
|
513
|
+
const remainingPages = context.pages?.() ?? [];
|
|
514
|
+
if (remainingPages.length === 0) {
|
|
515
|
+
handleDisconnect();
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Set up close event listener for a thread's Stagehand instance.
|
|
525
|
+
* Uses CDP Target.targetDestroyed events to detect when all pages are gone.
|
|
526
|
+
*/
|
|
527
|
+
setupCloseListenerForThread(stagehand, threadId) {
|
|
528
|
+
let disconnectHandled = false;
|
|
529
|
+
const handleDisconnect = () => {
|
|
530
|
+
if (disconnectHandled) return;
|
|
531
|
+
disconnectHandled = true;
|
|
532
|
+
this.handleThreadBrowserDisconnected(threadId);
|
|
533
|
+
};
|
|
534
|
+
try {
|
|
535
|
+
const stagehandAny = stagehand;
|
|
536
|
+
const conn = stagehandAny.ctx?.conn;
|
|
537
|
+
if (!conn?.on) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const pageTargets = /* @__PURE__ */ new Set();
|
|
541
|
+
const context = stagehand.context;
|
|
542
|
+
if (context) {
|
|
543
|
+
const pages = context.pages?.() ?? [];
|
|
544
|
+
for (const page of pages) {
|
|
545
|
+
const pageAny = page;
|
|
546
|
+
const targetId = pageAny._targetId ?? pageAny.targetId;
|
|
547
|
+
if (targetId) {
|
|
548
|
+
pageTargets.add(targetId);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
conn.on("Target.targetCreated", (params) => {
|
|
553
|
+
if (params.targetInfo.type === "page") {
|
|
554
|
+
pageTargets.add(params.targetInfo.targetId);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
conn.on("Target.targetDestroyed", (params) => {
|
|
558
|
+
if (pageTargets.has(params.targetId)) {
|
|
559
|
+
pageTargets.delete(params.targetId);
|
|
560
|
+
if (pageTargets.size === 0) {
|
|
561
|
+
handleDisconnect();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Handle browser disconnection for a specific thread.
|
|
570
|
+
* Called when a thread's browser is closed externally.
|
|
571
|
+
*/
|
|
572
|
+
handleThreadBrowserDisconnected(threadId) {
|
|
573
|
+
this.threadManager.clearSession(threadId);
|
|
574
|
+
this.logger.debug?.(`Cleared Stagehand session for thread: ${threadId}`);
|
|
575
|
+
this.notifyBrowserClosed(threadId);
|
|
576
|
+
}
|
|
577
|
+
async doClose() {
|
|
578
|
+
await this.threadManager.destroyAll();
|
|
579
|
+
if (this.stagehand) {
|
|
580
|
+
await this.stagehand.close();
|
|
581
|
+
this.stagehand = null;
|
|
582
|
+
}
|
|
583
|
+
this.setCurrentThread(void 0);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Check if the browser is still alive by verifying the context and pages exist.
|
|
587
|
+
* Called by base class ensureReady() to detect externally closed browsers.
|
|
588
|
+
*/
|
|
589
|
+
async checkBrowserAlive() {
|
|
590
|
+
const scope = this.getScope();
|
|
591
|
+
if (scope === "thread") {
|
|
592
|
+
return this.threadManager.hasActiveThreadStagehands();
|
|
593
|
+
}
|
|
594
|
+
if (!this.stagehand) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const context = this.stagehand.context;
|
|
599
|
+
if (!context) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const pages = context.pages();
|
|
603
|
+
if (!pages || pages.length === 0) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
const url = pages[0]?.url();
|
|
607
|
+
if (url && url !== "about:blank") {
|
|
608
|
+
const state = this.getBrowserStateFromStagehand(this.stagehand);
|
|
609
|
+
if (state) {
|
|
610
|
+
this.lastBrowserState = state;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
616
|
+
if (this.isDisconnectionError(msg)) {
|
|
617
|
+
this.logger.debug?.("Browser was externally closed");
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Handle browser disconnection by clearing internal state.
|
|
624
|
+
* For 'thread' scope, only notifies the specific thread's callbacks.
|
|
625
|
+
* For 'shared' scope, notifies all callbacks.
|
|
626
|
+
*/
|
|
627
|
+
handleBrowserDisconnected() {
|
|
628
|
+
const scope = this.threadManager.getScope();
|
|
629
|
+
const threadId = this.getCurrentThread();
|
|
630
|
+
if (scope === "thread" && threadId !== DEFAULT_THREAD_ID) {
|
|
631
|
+
this.threadManager.clearSession(threadId);
|
|
632
|
+
this.logger.debug?.(`Cleared Stagehand session for thread: ${threadId}`);
|
|
633
|
+
this.notifyBrowserClosed(threadId);
|
|
634
|
+
} else {
|
|
635
|
+
this.stagehand = null;
|
|
636
|
+
this.threadManager.clearStagehand();
|
|
637
|
+
super.handleBrowserDisconnected();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Create an error response from an exception.
|
|
642
|
+
* Extends base class to add Stagehand-specific error handling.
|
|
643
|
+
*/
|
|
644
|
+
createErrorFromException(error, context) {
|
|
645
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
646
|
+
if (msg.includes("No actions found") || msg.includes("Could not find")) {
|
|
647
|
+
return this.createError(
|
|
648
|
+
"element_not_found",
|
|
649
|
+
`${context}: Could not find matching element or action.`,
|
|
650
|
+
"Try rephrasing the instruction or use observe() to see available actions."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return super.createErrorFromException(error, context);
|
|
654
|
+
}
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// Internal Helpers
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
/**
|
|
659
|
+
* Get the Stagehand instance for a thread, creating it if needed.
|
|
660
|
+
* For 'browser' isolation, this creates a dedicated Stagehand instance.
|
|
661
|
+
* For 'none' isolation, returns the shared instance.
|
|
662
|
+
*/
|
|
663
|
+
async getStagehandForThread(threadId) {
|
|
664
|
+
const scope = this.getScope();
|
|
665
|
+
if (scope === "shared") {
|
|
666
|
+
return this.stagehand;
|
|
667
|
+
}
|
|
668
|
+
if (!threadId || threadId === DEFAULT_THREAD_ID) {
|
|
669
|
+
return this.stagehand;
|
|
670
|
+
}
|
|
671
|
+
let stagehand = this.threadManager.getStagehandForThread(threadId);
|
|
672
|
+
if (!stagehand) {
|
|
673
|
+
await this.threadManager.getManagerForThread(threadId);
|
|
674
|
+
stagehand = this.threadManager.getStagehandForThread(threadId);
|
|
675
|
+
}
|
|
676
|
+
return stagehand ?? null;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Require a Stagehand instance for the given or current thread.
|
|
680
|
+
* Throws if no instance is available.
|
|
681
|
+
* @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
|
|
682
|
+
* Use this to avoid race conditions in concurrent tool calls.
|
|
683
|
+
*/
|
|
684
|
+
requireStagehand(explicitThreadId) {
|
|
685
|
+
const threadId = explicitThreadId ?? this.getCurrentThread();
|
|
686
|
+
const stagehand = this.threadManager.getStagehandForThread(threadId ?? "") ?? this.stagehand;
|
|
687
|
+
if (!stagehand) {
|
|
688
|
+
throw new Error("Browser not launched");
|
|
689
|
+
}
|
|
690
|
+
return stagehand;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get the current page from Stagehand v3, respecting thread isolation.
|
|
694
|
+
* @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
|
|
695
|
+
* Use this to avoid race conditions in concurrent tool calls.
|
|
696
|
+
*/
|
|
697
|
+
getPage(explicitThreadId) {
|
|
698
|
+
const scope = this.getScope();
|
|
699
|
+
const threadId = explicitThreadId ?? this.getCurrentThread();
|
|
700
|
+
if (scope === "thread" && threadId && threadId !== DEFAULT_THREAD_ID) {
|
|
701
|
+
const stagehand = this.threadManager.getStagehandForThread(threadId);
|
|
702
|
+
if (stagehand?.context) {
|
|
703
|
+
return stagehand.context.activePage();
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
if (!this.stagehand) return null;
|
|
708
|
+
try {
|
|
709
|
+
const context = this.stagehand.context;
|
|
710
|
+
if (context) {
|
|
711
|
+
const activePage = context.activePage();
|
|
712
|
+
if (activePage) {
|
|
713
|
+
return activePage;
|
|
714
|
+
}
|
|
715
|
+
const pages = context.pages();
|
|
716
|
+
if (pages && pages.length > 0) {
|
|
717
|
+
return pages[0];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get the page for a specific thread, creating session if needed.
|
|
726
|
+
*/
|
|
727
|
+
async getPageForThread(threadId) {
|
|
728
|
+
const scope = this.threadManager.getScope();
|
|
729
|
+
if (scope === "shared") {
|
|
730
|
+
return this.getPage();
|
|
731
|
+
}
|
|
732
|
+
const stagehand = await this.getStagehandForThread(threadId);
|
|
733
|
+
if (stagehand?.context) {
|
|
734
|
+
return stagehand.context.activePage();
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Get a CDP session for a specific page.
|
|
740
|
+
*/
|
|
741
|
+
getCdpSessionForPage(page) {
|
|
742
|
+
if (!page) return null;
|
|
743
|
+
try {
|
|
744
|
+
const mainFrameId = page.mainFrameId?.();
|
|
745
|
+
if (mainFrameId && page.getSessionForFrame) {
|
|
746
|
+
return page.getSessionForFrame(mainFrameId);
|
|
747
|
+
}
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// Tools - Implements MastraBrowser.getTools()
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
getTools() {
|
|
756
|
+
return createStagehandTools(this);
|
|
757
|
+
}
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
// Core AI Methods
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
/**
|
|
762
|
+
* Perform an action using natural language instruction
|
|
763
|
+
* @param input - Action input
|
|
764
|
+
* @param threadId - Optional thread ID for thread-safe operation
|
|
765
|
+
*/
|
|
766
|
+
async act(input, threadId) {
|
|
767
|
+
const stagehand = this.requireStagehand(threadId);
|
|
768
|
+
const page = this.getPage(threadId);
|
|
769
|
+
const url = page?.url() ?? "";
|
|
770
|
+
try {
|
|
771
|
+
const result = await stagehand.act(input.instruction, {
|
|
772
|
+
variables: input.variables,
|
|
773
|
+
timeout: input.timeout,
|
|
774
|
+
page: page ?? void 0
|
|
775
|
+
});
|
|
776
|
+
return {
|
|
777
|
+
success: result.success,
|
|
778
|
+
message: result.message,
|
|
779
|
+
action: result.actionDescription,
|
|
780
|
+
url: page?.url() ?? url,
|
|
781
|
+
hint: "Use observe() to discover available actions or extract() to get page data."
|
|
782
|
+
};
|
|
783
|
+
} catch (error) {
|
|
784
|
+
return this.createErrorFromException(error, "Act");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Extract structured data from a page using natural language
|
|
789
|
+
* @param input - Extract input
|
|
790
|
+
* @param threadId - Optional thread ID for thread-safe operation
|
|
791
|
+
*/
|
|
792
|
+
async extract(input, threadId) {
|
|
793
|
+
const stagehand = this.requireStagehand(threadId);
|
|
794
|
+
const page = this.getPage(threadId);
|
|
795
|
+
const url = page?.url() ?? "";
|
|
796
|
+
try {
|
|
797
|
+
const options = { page: page ?? void 0 };
|
|
798
|
+
const result = input.schema ? await stagehand.extract(input.instruction, input.schema, options) : await stagehand.extract(input.instruction, options);
|
|
799
|
+
return {
|
|
800
|
+
success: true,
|
|
801
|
+
data: result,
|
|
802
|
+
url: page?.url() ?? url,
|
|
803
|
+
hint: "Data extracted successfully. Use act() to perform actions based on this data."
|
|
804
|
+
};
|
|
805
|
+
} catch (error) {
|
|
806
|
+
return this.createErrorFromException(error, "Extract");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Discover actionable elements on a page
|
|
811
|
+
* @param input - Observe input
|
|
812
|
+
* @param threadId - Optional thread ID for thread-safe operation
|
|
813
|
+
*/
|
|
814
|
+
async observe(input, threadId) {
|
|
815
|
+
const stagehand = this.requireStagehand(threadId);
|
|
816
|
+
const page = this.getPage(threadId);
|
|
817
|
+
const url = page?.url() ?? "";
|
|
818
|
+
try {
|
|
819
|
+
const options = { page: page ?? void 0 };
|
|
820
|
+
const actions = input.instruction ? await stagehand.observe(input.instruction, options) : await stagehand.observe(options);
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
actions: actions.map((a) => ({
|
|
824
|
+
selector: a.selector,
|
|
825
|
+
description: a.description,
|
|
826
|
+
method: a.method,
|
|
827
|
+
arguments: a.arguments
|
|
828
|
+
})),
|
|
829
|
+
url: page?.url() ?? url,
|
|
830
|
+
hint: actions.length > 0 ? `Found ${actions.length} actions. Use act() with a specific instruction to execute one.` : "No actions found. Try a different instruction or navigate to a different page."
|
|
831
|
+
};
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return this.createErrorFromException(error, "Observe");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// ---------------------------------------------------------------------------
|
|
837
|
+
// Navigation & State Methods
|
|
838
|
+
// ---------------------------------------------------------------------------
|
|
839
|
+
/**
|
|
840
|
+
* Navigate to a URL
|
|
841
|
+
* @param input - Navigate input
|
|
842
|
+
* @param threadId - Optional thread ID for thread-safe operation
|
|
843
|
+
*/
|
|
844
|
+
async navigate(input, threadId) {
|
|
845
|
+
const page = this.getPage(threadId);
|
|
846
|
+
if (!page) {
|
|
847
|
+
return this.createError("browser_error", "Browser page not available.", "Ensure the browser is launched.");
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
await page.goto(input.url, {
|
|
851
|
+
waitUntil: input.waitUntil ?? "domcontentloaded"
|
|
852
|
+
});
|
|
853
|
+
const url = page.url();
|
|
854
|
+
const title = await page.title();
|
|
855
|
+
return {
|
|
856
|
+
success: true,
|
|
857
|
+
url,
|
|
858
|
+
title,
|
|
859
|
+
hint: "Page loaded. Use observe() to discover actions or extract() to get data."
|
|
860
|
+
};
|
|
861
|
+
} catch (error) {
|
|
862
|
+
return this.createErrorFromException(error, "Navigate");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
// Tab Management
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
/**
|
|
869
|
+
* Manage browser tabs - list, create, switch, close
|
|
870
|
+
* @param input - Tabs input
|
|
871
|
+
* @param threadId - Optional thread ID for thread-safe operation
|
|
872
|
+
*/
|
|
873
|
+
async tabs(input, threadId) {
|
|
874
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread();
|
|
875
|
+
const stagehand = this.requireStagehand(effectiveThreadId);
|
|
876
|
+
const context = stagehand.context;
|
|
877
|
+
if (!context) {
|
|
878
|
+
return this.createError("browser_error", "Browser context not available.", "Ensure the browser is launched.");
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
switch (input.action) {
|
|
882
|
+
case "list": {
|
|
883
|
+
const pages = context.pages();
|
|
884
|
+
const activePage = context.activePage();
|
|
885
|
+
const tabs = await Promise.all(
|
|
886
|
+
pages.map(async (page, index) => ({
|
|
887
|
+
index,
|
|
888
|
+
url: page.url(),
|
|
889
|
+
title: await page.title(),
|
|
890
|
+
active: page === activePage
|
|
891
|
+
}))
|
|
892
|
+
);
|
|
893
|
+
return {
|
|
894
|
+
success: true,
|
|
895
|
+
tabs,
|
|
896
|
+
hint: 'Use stagehand_tabs with action:"switch" and index to change tabs.'
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
case "new": {
|
|
900
|
+
const newPage = await context.newPage(input.url);
|
|
901
|
+
await this.reconnectScreencastForThread(effectiveThreadId, "new tab via tool");
|
|
902
|
+
this.updateSessionBrowserState(effectiveThreadId);
|
|
903
|
+
return {
|
|
904
|
+
success: true,
|
|
905
|
+
index: context.pages().length - 1,
|
|
906
|
+
url: newPage.url(),
|
|
907
|
+
title: await newPage.title(),
|
|
908
|
+
hint: "New tab opened. Use stagehand_observe to discover actions."
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
case "switch": {
|
|
912
|
+
if (input.index === void 0) {
|
|
913
|
+
return this.createError(
|
|
914
|
+
"browser_error",
|
|
915
|
+
"Tab index required for switch action.",
|
|
916
|
+
"Provide index parameter."
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const pages = context.pages();
|
|
920
|
+
if (input.index < 0 || input.index >= pages.length) {
|
|
921
|
+
return this.createError(
|
|
922
|
+
"browser_error",
|
|
923
|
+
`Invalid tab index: ${input.index}. Valid range: 0-${pages.length - 1}`,
|
|
924
|
+
'Use stagehand_tabs with action:"list" to see available tabs.'
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const targetPage = pages[input.index];
|
|
928
|
+
const targetUrl = targetPage.url();
|
|
929
|
+
context.setActivePage(targetPage);
|
|
930
|
+
await this.reconnectScreencastForThread(effectiveThreadId, "tab switch via tool");
|
|
931
|
+
const streamKey = this.getStreamKey(effectiveThreadId);
|
|
932
|
+
const stream = this.activeScreencastStreams.get(streamKey);
|
|
933
|
+
if (targetUrl && stream?.isActive()) {
|
|
934
|
+
stream.emitUrl(targetUrl);
|
|
935
|
+
}
|
|
936
|
+
this.updateSessionBrowserState(effectiveThreadId);
|
|
937
|
+
return {
|
|
938
|
+
success: true,
|
|
939
|
+
index: input.index,
|
|
940
|
+
url: targetUrl,
|
|
941
|
+
title: await targetPage.title(),
|
|
942
|
+
hint: "Tab switched. Use stagehand_observe to discover actions."
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
case "close": {
|
|
946
|
+
const pages = context.pages();
|
|
947
|
+
const indexToClose = input.index ?? pages.findIndex((p) => p === context.activePage());
|
|
948
|
+
if (indexToClose < 0 || indexToClose >= pages.length) {
|
|
949
|
+
return this.createError(
|
|
950
|
+
"browser_error",
|
|
951
|
+
`Invalid tab index: ${indexToClose}`,
|
|
952
|
+
'Use stagehand_tabs with action:"list" to see available tabs.'
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const pageToClose = pages[indexToClose];
|
|
956
|
+
await pageToClose.close();
|
|
957
|
+
await this.reconnectScreencastForThread(effectiveThreadId, "tab close via tool");
|
|
958
|
+
this.updateSessionBrowserState(effectiveThreadId);
|
|
959
|
+
const remainingPages = context.pages();
|
|
960
|
+
return {
|
|
961
|
+
success: true,
|
|
962
|
+
remaining: remainingPages.length,
|
|
963
|
+
hint: remainingPages.length > 0 ? "Tab closed. Use stagehand_observe to see current tab." : "All tabs closed."
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
default:
|
|
967
|
+
return this.createError(
|
|
968
|
+
"browser_error",
|
|
969
|
+
`Unknown tabs action: ${input.action}`,
|
|
970
|
+
'Use "list", "new", "switch", or "close".'
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
} catch (error) {
|
|
974
|
+
return this.createErrorFromException(error, "Tabs");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// ---------------------------------------------------------------------------
|
|
978
|
+
// URL Tracking (for Studio browser view)
|
|
979
|
+
// ---------------------------------------------------------------------------
|
|
980
|
+
async getCurrentUrl(threadId) {
|
|
981
|
+
if (!this.isBrowserRunning()) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread();
|
|
985
|
+
const scope = this.threadManager.getScope();
|
|
986
|
+
if (scope === "thread" && effectiveThreadId) {
|
|
987
|
+
const stagehand = this.threadManager.getStagehandForThread(effectiveThreadId);
|
|
988
|
+
if (!stagehand?.context) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const page2 = stagehand.context.activePage();
|
|
992
|
+
const url = page2?.url() ?? null;
|
|
993
|
+
if (url && url !== "about:blank") {
|
|
994
|
+
const state = this.getBrowserStateFromStagehand(stagehand);
|
|
995
|
+
if (state) {
|
|
996
|
+
this.threadManager.updateBrowserState(effectiveThreadId, state);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return url;
|
|
1000
|
+
}
|
|
1001
|
+
const page = this.getPage();
|
|
1002
|
+
if (!page) return null;
|
|
1003
|
+
try {
|
|
1004
|
+
const url = page.url();
|
|
1005
|
+
if (url && url !== "about:blank") {
|
|
1006
|
+
const state = this.getBrowserStateFromStagehand(this.stagehand);
|
|
1007
|
+
if (state) {
|
|
1008
|
+
this.lastBrowserState = state;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return url;
|
|
1012
|
+
} catch {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Navigate to a URL (simple version). Used internally for restoring state on relaunch.
|
|
1018
|
+
*/
|
|
1019
|
+
async navigateTo(url) {
|
|
1020
|
+
const page = this.getPage();
|
|
1021
|
+
if (!page) return;
|
|
1022
|
+
try {
|
|
1023
|
+
await page.goto(url, {
|
|
1024
|
+
timeoutMs: this.config.timeout ?? 3e4,
|
|
1025
|
+
waitUntil: "domcontentloaded"
|
|
1026
|
+
});
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Get the current browser state (all tabs and active tab index).
|
|
1032
|
+
*/
|
|
1033
|
+
async getBrowserState(threadId) {
|
|
1034
|
+
if (!this.isBrowserRunning()) {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const scope = this.threadManager.getScope();
|
|
1039
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread();
|
|
1040
|
+
if (scope === "thread" && effectiveThreadId) {
|
|
1041
|
+
const stagehand = this.threadManager.getStagehandForThread(effectiveThreadId);
|
|
1042
|
+
if (!stagehand) return null;
|
|
1043
|
+
return this.getBrowserStateFromStagehand(stagehand);
|
|
1044
|
+
}
|
|
1045
|
+
return this.getBrowserStateFromStagehand(this.stagehand);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Get browser state from a specific Stagehand instance.
|
|
1052
|
+
*/
|
|
1053
|
+
getBrowserStateFromStagehand(stagehand) {
|
|
1054
|
+
if (!stagehand?.context) return null;
|
|
1055
|
+
try {
|
|
1056
|
+
const pages = stagehand.context.pages();
|
|
1057
|
+
const activePage = stagehand.context.activePage();
|
|
1058
|
+
let activeIndex = 0;
|
|
1059
|
+
const tabs = pages.map((page, index) => {
|
|
1060
|
+
if (page === activePage) {
|
|
1061
|
+
activeIndex = index;
|
|
1062
|
+
}
|
|
1063
|
+
return { url: page.url() };
|
|
1064
|
+
});
|
|
1065
|
+
return {
|
|
1066
|
+
tabs,
|
|
1067
|
+
activeTabIndex: activeIndex
|
|
1068
|
+
};
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get all open tabs with their URLs and titles.
|
|
1075
|
+
*/
|
|
1076
|
+
async getTabState(threadId) {
|
|
1077
|
+
const state = await this.getBrowserState(threadId);
|
|
1078
|
+
return state?.tabs ?? [];
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Get the active tab index.
|
|
1082
|
+
*/
|
|
1083
|
+
async getActiveTabIndex(threadId) {
|
|
1084
|
+
const state = await this.getBrowserState(threadId);
|
|
1085
|
+
return state?.activeTabIndex ?? 0;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Update the browser state in the thread session.
|
|
1089
|
+
* Called on navigation, tab open/close to keep state fresh.
|
|
1090
|
+
*/
|
|
1091
|
+
updateSessionBrowserState(threadId) {
|
|
1092
|
+
try {
|
|
1093
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread() ?? DEFAULT_THREAD_ID;
|
|
1094
|
+
const scope = this.threadManager.getScope();
|
|
1095
|
+
let stagehand = null;
|
|
1096
|
+
if (scope === "thread") {
|
|
1097
|
+
stagehand = this.threadManager.getStagehandForThread(effectiveThreadId);
|
|
1098
|
+
} else {
|
|
1099
|
+
stagehand = this.stagehand;
|
|
1100
|
+
}
|
|
1101
|
+
if (stagehand) {
|
|
1102
|
+
const state = this.getBrowserStateFromStagehand(stagehand);
|
|
1103
|
+
if (state) {
|
|
1104
|
+
this.threadManager.updateBrowserState(effectiveThreadId, state);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// Screencast (for Studio live view)
|
|
1112
|
+
// Uses Stagehand v3's native CDP access
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
/**
|
|
1115
|
+
* Get the stream key for a thread (or shared key for shared scope).
|
|
1116
|
+
*/
|
|
1117
|
+
getStreamKey(threadId) {
|
|
1118
|
+
return threadId || _StagehandBrowser.SHARED_STREAM_KEY;
|
|
1119
|
+
}
|
|
1120
|
+
async startScreencast(options) {
|
|
1121
|
+
const threadId = options?.threadId;
|
|
1122
|
+
const provider = {
|
|
1123
|
+
getCdpSession: async () => {
|
|
1124
|
+
const page = await this.getPageForThread(threadId ?? "");
|
|
1125
|
+
if (!page) {
|
|
1126
|
+
throw new Error("No page available for screencast");
|
|
1127
|
+
}
|
|
1128
|
+
const session = this.getCdpSessionForPage(page);
|
|
1129
|
+
if (!session) {
|
|
1130
|
+
throw new Error("No CDP session available for page");
|
|
1131
|
+
}
|
|
1132
|
+
return session;
|
|
1133
|
+
},
|
|
1134
|
+
isBrowserRunning: () => this.isBrowserRunning()
|
|
1135
|
+
};
|
|
1136
|
+
const stream = new ScreencastStreamImpl(provider, options);
|
|
1137
|
+
const streamKey = this.getStreamKey(threadId);
|
|
1138
|
+
this.activeScreencastStreams.set(streamKey, stream);
|
|
1139
|
+
await stream.start();
|
|
1140
|
+
await this.setupTabChangeDetection(threadId, stream);
|
|
1141
|
+
stream.once("stop", () => {
|
|
1142
|
+
if (this.activeScreencastStreams.get(streamKey) === stream) {
|
|
1143
|
+
this.activeScreencastStreams.delete(streamKey);
|
|
1144
|
+
}
|
|
1145
|
+
const timer = this.tabChangeDebounceTimers.get(streamKey);
|
|
1146
|
+
if (timer) {
|
|
1147
|
+
clearTimeout(timer);
|
|
1148
|
+
this.tabChangeDebounceTimers.delete(streamKey);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
return stream;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Set up listeners to detect tab changes and reconnect the screencast.
|
|
1155
|
+
* Uses CDP Target events since Stagehand doesn't expose page lifecycle events.
|
|
1156
|
+
*/
|
|
1157
|
+
async setupTabChangeDetection(threadId, stream) {
|
|
1158
|
+
const stagehand = await this.getStagehandForThread(threadId);
|
|
1159
|
+
if (!stagehand?.context) return;
|
|
1160
|
+
const connection = stagehand.context.conn;
|
|
1161
|
+
if (!connection) {
|
|
1162
|
+
this.logger.debug?.("No CDP connection available for tab change detection");
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const targetSessions = /* @__PURE__ */ new Map();
|
|
1166
|
+
let targetInfoDebounceTimer = null;
|
|
1167
|
+
const isTrackedByStagehand = (targetId) => {
|
|
1168
|
+
const pages = stagehand.context?.pages() || [];
|
|
1169
|
+
return pages.some((p) => p.targetId() === targetId);
|
|
1170
|
+
};
|
|
1171
|
+
const streamKey = this.getStreamKey(threadId);
|
|
1172
|
+
const onTargetCreated = (params) => {
|
|
1173
|
+
if (params.targetInfo.type !== "page") return;
|
|
1174
|
+
const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
|
|
1175
|
+
if (existingTimer) {
|
|
1176
|
+
clearTimeout(existingTimer);
|
|
1177
|
+
}
|
|
1178
|
+
this.tabChangeDebounceTimers.set(
|
|
1179
|
+
streamKey,
|
|
1180
|
+
setTimeout(() => {
|
|
1181
|
+
this.tabChangeDebounceTimers.delete(streamKey);
|
|
1182
|
+
void this.reconnectScreencastForThread(threadId, "new tab");
|
|
1183
|
+
void setupPageNavigationListener();
|
|
1184
|
+
}, 300)
|
|
1185
|
+
);
|
|
1186
|
+
};
|
|
1187
|
+
const onTargetAttached = (params) => {
|
|
1188
|
+
if (params.targetInfo.type !== "page") return;
|
|
1189
|
+
targetSessions.set(params.targetInfo.targetId, params.sessionId);
|
|
1190
|
+
};
|
|
1191
|
+
let pendingTargetInfo = null;
|
|
1192
|
+
const onTargetInfoChanged = (params) => {
|
|
1193
|
+
if (params.targetInfo.type !== "page") return;
|
|
1194
|
+
if (isTrackedByStagehand(params.targetInfo.targetId)) return;
|
|
1195
|
+
const sessionId = targetSessions.get(params.targetInfo.targetId);
|
|
1196
|
+
if (!sessionId) return;
|
|
1197
|
+
pendingTargetInfo = params;
|
|
1198
|
+
if (targetInfoDebounceTimer) {
|
|
1199
|
+
clearTimeout(targetInfoDebounceTimer);
|
|
1200
|
+
}
|
|
1201
|
+
targetInfoDebounceTimer = setTimeout(async () => {
|
|
1202
|
+
targetInfoDebounceTimer = null;
|
|
1203
|
+
if (!pendingTargetInfo) return;
|
|
1204
|
+
const info = pendingTargetInfo.targetInfo;
|
|
1205
|
+
const sid = targetSessions.get(info.targetId);
|
|
1206
|
+
pendingTargetInfo = null;
|
|
1207
|
+
if (isTrackedByStagehand(info.targetId) || !sid) return;
|
|
1208
|
+
const contextAny = stagehand.context;
|
|
1209
|
+
if (contextAny?.onAttachedToTarget) {
|
|
1210
|
+
try {
|
|
1211
|
+
await contextAny.onAttachedToTarget(info, sid);
|
|
1212
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1213
|
+
if (isTrackedByStagehand(info.targetId)) {
|
|
1214
|
+
this.logger.debug?.("Page registered successfully, setting as active");
|
|
1215
|
+
const pages = stagehand.context?.pages() || [];
|
|
1216
|
+
const newPage = pages.find((p) => p.targetId() === info.targetId);
|
|
1217
|
+
if (newPage && stagehand.context) {
|
|
1218
|
+
stagehand.context.setActivePage(newPage);
|
|
1219
|
+
}
|
|
1220
|
+
void this.reconnectScreencast("manual tab tracked");
|
|
1221
|
+
void setupPageNavigationListener();
|
|
1222
|
+
} else {
|
|
1223
|
+
this.logger.debug?.("Stagehand did not register the page (non-injectable URL)");
|
|
1224
|
+
}
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
this.logger.debug?.("Failed to register page with Stagehand", e);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}, 300);
|
|
1230
|
+
};
|
|
1231
|
+
const onTargetDestroyed = (params) => {
|
|
1232
|
+
this.logger.debug?.("Page target destroyed");
|
|
1233
|
+
targetSessions.delete(params.targetId);
|
|
1234
|
+
const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
|
|
1235
|
+
if (existingTimer) {
|
|
1236
|
+
clearTimeout(existingTimer);
|
|
1237
|
+
}
|
|
1238
|
+
this.tabChangeDebounceTimers.set(
|
|
1239
|
+
streamKey,
|
|
1240
|
+
setTimeout(() => {
|
|
1241
|
+
this.tabChangeDebounceTimers.delete(streamKey);
|
|
1242
|
+
void this.reconnectScreencastForThread(threadId, "tab closed");
|
|
1243
|
+
void setupPageNavigationListener();
|
|
1244
|
+
}, 300)
|
|
1245
|
+
);
|
|
1246
|
+
};
|
|
1247
|
+
const onFrameNavigated = (params) => {
|
|
1248
|
+
if (!params.frame.parentId && params.frame.url) {
|
|
1249
|
+
stream.emitUrl(params.frame.url);
|
|
1250
|
+
this.updateSessionBrowserState(threadId);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
let pageSession = null;
|
|
1254
|
+
const setupPageNavigationListener = async () => {
|
|
1255
|
+
try {
|
|
1256
|
+
if (pageSession?.off) {
|
|
1257
|
+
pageSession.off("Page.frameNavigated", onFrameNavigated);
|
|
1258
|
+
}
|
|
1259
|
+
const page = stagehand.context?.activePage();
|
|
1260
|
+
if (!page) return;
|
|
1261
|
+
const session = page.getSessionForFrame(page.mainFrameId());
|
|
1262
|
+
if (!session) return;
|
|
1263
|
+
pageSession = session;
|
|
1264
|
+
await session.send("Page.enable");
|
|
1265
|
+
session.on("Page.frameNavigated", onFrameNavigated);
|
|
1266
|
+
const currentUrl = page.url();
|
|
1267
|
+
if (currentUrl && currentUrl !== "about:blank") {
|
|
1268
|
+
stream.emitUrl(currentUrl);
|
|
1269
|
+
}
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
this.logger.debug?.("Failed to set up page navigation listener", error);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
const cleanup = () => {
|
|
1275
|
+
const timer = this.tabChangeDebounceTimers.get(streamKey);
|
|
1276
|
+
if (timer) {
|
|
1277
|
+
clearTimeout(timer);
|
|
1278
|
+
this.tabChangeDebounceTimers.delete(streamKey);
|
|
1279
|
+
}
|
|
1280
|
+
if (targetInfoDebounceTimer) {
|
|
1281
|
+
clearTimeout(targetInfoDebounceTimer);
|
|
1282
|
+
targetInfoDebounceTimer = null;
|
|
1283
|
+
}
|
|
1284
|
+
connection.off?.("Target.targetCreated", onTargetCreated);
|
|
1285
|
+
connection.off?.("Target.targetDestroyed", onTargetDestroyed);
|
|
1286
|
+
connection.off?.("Target.attachedToTarget", onTargetAttached);
|
|
1287
|
+
connection.off?.("Target.targetInfoChanged", onTargetInfoChanged);
|
|
1288
|
+
if (pageSession?.off) {
|
|
1289
|
+
pageSession.off("Page.frameNavigated", onFrameNavigated);
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
stream.once("stop", cleanup);
|
|
1293
|
+
try {
|
|
1294
|
+
connection.on?.("Target.targetCreated", onTargetCreated);
|
|
1295
|
+
connection.on?.("Target.targetDestroyed", onTargetDestroyed);
|
|
1296
|
+
connection.on?.("Target.attachedToTarget", onTargetAttached);
|
|
1297
|
+
connection.on?.("Target.targetInfoChanged", onTargetInfoChanged);
|
|
1298
|
+
await setupPageNavigationListener();
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
this.logger.debug?.("Failed to set up tab change detection", error);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Reconnect the active screencast for a specific thread.
|
|
1305
|
+
*/
|
|
1306
|
+
async reconnectScreencastForThread(threadId, reason) {
|
|
1307
|
+
const streamKey = this.getStreamKey(threadId);
|
|
1308
|
+
const stream = this.activeScreencastStreams.get(streamKey);
|
|
1309
|
+
if (!stream || !stream.isActive()) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (!this.isBrowserRunning()) {
|
|
1313
|
+
this.logger.debug?.("Skipping screencast reconnect - browser not running");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const scope = this.getScope();
|
|
1317
|
+
if (scope === "thread" && threadId && !this.threadManager.getStagehandForThread(threadId)) {
|
|
1318
|
+
this.logger.debug?.(`Skipping screencast reconnect - no session for thread ${threadId}`);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
this.logger.debug?.(`Reconnecting screencast: ${reason}`);
|
|
1322
|
+
try {
|
|
1323
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1324
|
+
await stream.reconnect();
|
|
1325
|
+
const stagehand = await this.getStagehandForThread(threadId);
|
|
1326
|
+
const activePage = stagehand?.context?.activePage();
|
|
1327
|
+
if (activePage) {
|
|
1328
|
+
const url = activePage.url();
|
|
1329
|
+
if (url) {
|
|
1330
|
+
stream.emitUrl(url);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
this.logger.debug?.("Screencast reconnect failed", error);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Reconnect the active screencast for the current thread.
|
|
1339
|
+
* Wrapper for reconnectScreencastForThread using getCurrentThread().
|
|
1340
|
+
*/
|
|
1341
|
+
async reconnectScreencast(reason) {
|
|
1342
|
+
const threadId = this.getCurrentThread();
|
|
1343
|
+
await this.reconnectScreencastForThread(threadId, reason);
|
|
1344
|
+
}
|
|
1345
|
+
// NOTE: Manual tab switching in browser UI is not fully supported.
|
|
1346
|
+
// Stagehand v3 does not track pages opened via browser UI (only pages created through its API).
|
|
1347
|
+
// We've requested this feature from Browserbase - see Notion doc for details.
|
|
1348
|
+
// ---------------------------------------------------------------------------
|
|
1349
|
+
// Event Injection (for Studio live view interactivity)
|
|
1350
|
+
// ---------------------------------------------------------------------------
|
|
1351
|
+
async injectMouseEvent(event, threadId) {
|
|
1352
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread();
|
|
1353
|
+
const page = await this.getPageForThread(effectiveThreadId ?? "");
|
|
1354
|
+
const cdpSession = this.getCdpSessionForPage(page);
|
|
1355
|
+
if (!cdpSession) {
|
|
1356
|
+
throw new Error("No CDP session available");
|
|
1357
|
+
}
|
|
1358
|
+
const buttonMap = {
|
|
1359
|
+
none: 0,
|
|
1360
|
+
left: 1,
|
|
1361
|
+
middle: 4,
|
|
1362
|
+
right: 2
|
|
1363
|
+
};
|
|
1364
|
+
const defaultClickCount = event.type === "mousePressed" || event.type === "mouseReleased" ? 1 : 0;
|
|
1365
|
+
await cdpSession.send("Input.dispatchMouseEvent", {
|
|
1366
|
+
type: event.type,
|
|
1367
|
+
x: event.x,
|
|
1368
|
+
y: event.y,
|
|
1369
|
+
button: event.button ?? "none",
|
|
1370
|
+
buttons: buttonMap[event.button ?? "none"] ?? 0,
|
|
1371
|
+
clickCount: event.clickCount ?? defaultClickCount,
|
|
1372
|
+
deltaX: event.deltaX ?? 0,
|
|
1373
|
+
deltaY: event.deltaY ?? 0,
|
|
1374
|
+
modifiers: event.modifiers ?? 0
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
async injectKeyboardEvent(event, threadId) {
|
|
1378
|
+
const effectiveThreadId = threadId ?? this.getCurrentThread();
|
|
1379
|
+
const page = await this.getPageForThread(effectiveThreadId ?? "");
|
|
1380
|
+
const cdpSession = this.getCdpSessionForPage(page);
|
|
1381
|
+
if (!cdpSession) {
|
|
1382
|
+
throw new Error("No CDP session available");
|
|
1383
|
+
}
|
|
1384
|
+
await cdpSession.send("Input.dispatchKeyEvent", {
|
|
1385
|
+
type: event.type,
|
|
1386
|
+
key: event.key,
|
|
1387
|
+
code: event.code,
|
|
1388
|
+
text: event.text,
|
|
1389
|
+
modifiers: event.modifiers ?? 0,
|
|
1390
|
+
windowsVirtualKeyCode: event.windowsVirtualKeyCode
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
export { STAGEHAND_TOOLS, StagehandBrowser, actInputSchema, closeInputSchema, createStagehandTools, extractInputSchema, navigateInputSchema, observeInputSchema, stagehandSchemas, tabsInputSchema };
|
|
1396
|
+
//# sourceMappingURL=index.js.map
|
|
1397
|
+
//# sourceMappingURL=index.js.map
|