From cd8ff378826c2af6510b5dbcf79feb647108bcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 4 Aug 2026 22:06:46 +0200 Subject: [PATCH] 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. --- src/browser/tools.zig | 21 ++++++++++++++------- src/script/Runtime.zig | 4 +++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 73f2ee8bb..f1871e400 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -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; diff --git a/src/script/Runtime.zig b/src/script/Runtime.zig index 1f76d98b0..ee4ceee41 100644 --- a/src/script/Runtime.zig +++ b/src/script/Runtime.zig @@ -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