tools: add waitUntil option to goto

Optional param on the goto tool (load | domcontentloaded | networkalmostidle |
networkidle), default unchanged (load). Threaded through performGoto, startGoto
and the script runtime's pending-goto driver, which previously hard-coded
.load. Closes half of #3138.
This commit is contained in:
Adrià Arrufat committed 2026-08-04 22:06:46 +02:00
1 parent e143533987
commit cd8ff37882
2 files changed
+17 -8

No files matched your search

+14 -7
View File
@@ -329,7 +329,8 @@ pub const Tool = enum {
\\ "type": "object",
\\ "properties": {
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." },
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkalmostidle", "networkidle"], "description": "Event that completes the navigation. Defaults to 'load'. Prefer 'domcontentloaded' followed by waitForSelector on pages whose late scripts (ads) hold 'load' back." }
\\ },
\\ "required": ["url"]
\\}
@@ -758,6 +759,7 @@ pub const ToolResult = struct {
pub const GotoParams = struct {
url: [:0]const u8,
timeout: ?u32 = null,
waitUntil: ?lp.Config.WaitUntil = null,
};
pub const UrlParams = struct {
@@ -936,7 +938,7 @@ const schema_walker_suffix = ")";
fn execGoto(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
const args = try parseArgs(GotoParams, arena, arguments);
return switch (try performGoto(session, registry, args.url, args.timeout)) {
return switch (try performGoto(session, registry, args.url, args.timeout, args.waitUntil)) {
.completed => "Navigated successfully.",
.timeout => "Navigation started but the page did not finish loading before the timeout.",
};
@@ -988,7 +990,7 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
.{encoded},
0,
) catch return ToolError.OutOfMemory;
_ = try performGoto(session, registry, ddg_url, args.timeout);
_ = try performGoto(session, registry, ddg_url, args.timeout, null);
const ddg_frame = try requireFrame(session);
return .{ .text = try renderFrameMarkdown(arena, ddg_frame) };
}
@@ -1921,7 +1923,7 @@ fn ensurePage(session: *lp.Session, registry: *CDPNode.Registry, url: ?[:0]const
if (session.currentFrame()) |frame| {
if (std.mem.eql(u8, frame.url, u)) return frame;
}
_ = try performGoto(session, registry, u, timeout);
_ = try performGoto(session, registry, u, timeout, null);
}
return session.currentFrame() orelse ToolError.FrameNotLoaded;
}
@@ -1937,6 +1939,7 @@ const default_nav_timeout_ms: u32 = 10000;
pub const StartedGoto = struct {
frame_id: u32,
timeout_ms: u32,
until: lp.Config.WaitUntil,
};
/// Open a fresh top-level page and start its navigation. The frame is non-null
@@ -1971,10 +1974,14 @@ pub fn startGoto(
}
}
const page = try openPage(session, args.url);
return .{ .frame_id = page.frame_id, .timeout_ms = args.timeout orelse default_nav_timeout_ms };
return .{
.frame_id = page.frame_id,
.timeout_ms = args.timeout orelse default_nav_timeout_ms,
.until = args.waitUntil orelse default_nav_wait,
};
}
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, timeout: ?u32) ToolError!lp.Session.Runner.WaitResult {
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, timeout: ?u32, wait_until: ?lp.Config.WaitUntil) ToolError!lp.Session.Runner.WaitResult {
if (session.primaryPage()) |old_page| {
registry.reset();
old_page.close();
@@ -1982,7 +1989,7 @@ fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const
const page = try openPage(session, url);
var runner = session.runner(.{});
const condition = lp.Session.Runner.WaitCondition{ .frame_id = page.frame_id, .until = default_nav_wait };
const condition = lp.Session.Runner.WaitCondition{ .frame_id = page.frame_id, .until = wait_until orelse default_nav_wait };
var conditions = [_]lp.Session.Runner.WaitCondition{condition};
const result = runner.waitResult(timeout orelse default_nav_timeout_ms, &conditions) catch |err| {
return if (err == error.Cancelled) ToolError.Cancelled else ToolError.NavigationFailed;
+3 -1
View File
@@ -74,6 +74,7 @@ const PendingGoto = struct {
receiver: v8.Global,
/// `run_timer` reading (ms) past which the navigation is abandoned.
deadline_ms: u64,
until: lp.Config.WaitUntil,
fn reset(self: *PendingGoto) void {
v8.v8__Global__Reset(&self.resolver);
@@ -513,6 +514,7 @@ fn invokeGoto(
.resolver = undefined,
.receiver = undefined,
.deadline_ms = @as(u64, @intCast(self.run_timer.untilNow(lp.io, .boot).toMilliseconds())) + started.timeout_ms,
.until = started.until,
};
v8.v8__Global__New(self.env.isolate.handle, resolver, &pending.resolver);
v8.v8__Global__New(self.env.isolate.handle, this, &pending.receiver);
@@ -551,7 +553,7 @@ fn driveAsync(self: *Runtime, context: *const v8.Context, try_catch: *const v8.T
break;
};
for (self.pending_gotos.items, conditions) |pending, *condition| {
condition.* = .{ .frame_id = pending.frame_id, .until = .load };
condition.* = .{ .frame_id = pending.frame_id, .until = pending.until };
}
// Browser-side tick: run under the browser's isolate, exit before the