diff --git a/src/Config.zig b/src/Config.zig index 61990c8b..b2e5725f 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -291,7 +291,7 @@ pub fn disableSubframes(self: *const Config) bool { pub fn disableWorkers(self: *const Config) bool { return switch (self.mode) { - inline .serve, .fetch, .mcp => |opts| opts.disable_workers, + inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_workers, else => unreachable, }; } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index c72ebabd..1c7f0bfd 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -255,7 +255,6 @@ _parent_notified: bool = false, _type: enum { root, frame }, // only used for logs right now _req_id: u32 = 0, -_console_messages: std.Io.Writer.Allocating, _navigated_options: ?NavigatedOpts = null, pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { @@ -291,7 +290,6 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { ._style_manager = undefined, ._script_manager = undefined, ._event_manager = EventManager.init(arena, self), - ._console_messages = .init(arena), }; self._to_load = &self._to_load_1; @@ -2867,35 +2865,6 @@ fn isXmlNameChar(c: u21) bool { (c >= 0x203F and c <= 0x2040); } -const max_console_bytes = 64 * 1024; - -pub fn appendConsoleMessage(self: *Frame, level: ConsoleLevel, values: []JS.Value) void { - const aw = &self._console_messages; - const start = aw.written().len; - if (start >= max_console_bytes) return; - appendConsoleMessageInner(&aw.writer, level, values) catch { - aw.shrinkRetainingCapacity(start); - }; -} - -fn appendConsoleMessageInner(w: *std.Io.Writer, level: ConsoleLevel, values: []JS.Value) !void { - try w.print("[{s}] ", .{@tagName(level)}); - for (values, 0..) |value, i| { - if (i > 0) try w.writeAll(" "); - try value.format(w); - } - try w.writeByte('\n'); -} - -/// Returns the buffered console output and clears the buffer. The returned -/// slice is valid until the next `appendConsoleMessage` reuses the backing -/// storage, so callers must consume or copy it before that happens. -pub fn drainConsoleMessages(self: *Frame) []const u8 { - const text = self._console_messages.written(); - self._console_messages.clearRetainingCapacity(); - return text; -} - pub fn dupeString(self: *Frame, value: []const u8) ![]const u8 { if (String.intern(value)) |v| { return v; @@ -3669,8 +3638,6 @@ pub const NavigateReason = enum { initialFrameNavigation, }; -pub const ConsoleLevel = enum { log, debug, info, warn, @"error" }; - pub const NavigateOpts = struct { cdp_id: ?i64 = null, reason: NavigateReason = .address_bar, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index be035d61..9639335c 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -89,6 +89,13 @@ subframe_loading_enabled: bool = true, // session init; the LP.configureLoading CDP method can flip it per-session. worker_loading_enabled: bool = true, +// Session-scoped capture of console.* output for the `consoleLogs` MCP tool. +// Fed by an `console_message` notification listener; drained on tool call. +// Capped at `max_console_bytes` so a runaway page can't grow it unbounded. +_console_messages: std.Io.Writer.Allocating, + +const max_console_bytes = 64 * 1024; + pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { const allocator = browser.app.allocator; const arena_pool = browser.arena_pool; @@ -110,10 +117,16 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi // CLI defaults; LP.configureLoading can flip these per-session. .subframe_loading_enabled = !browser.app.config.disableSubframes(), .worker_loading_enabled = !browser.app.config.disableWorkers(), + ._console_messages = .init(allocator), }; + errdefer self._console_messages.deinit(); + + try notification.register(.console_message, self, onConsoleMessage); } pub fn deinit(self: *Session) void { + self.notification.unregister(.console_message, self); + if (self._pending != null) { self.discardPendingPage(); } @@ -132,9 +145,38 @@ pub fn deinit(self: *Session) void { self.fc_identity_pool.deinit(); self.storage_shed.deinit(self.browser.app.allocator); + self._console_messages.deinit(); self.arena_pool.release(self.arena); } +fn onConsoleMessage(ctx: *anyopaque, msg: *const Notification.ConsoleMessage) !void { + const self: *Session = @ptrCast(@alignCast(ctx)); + const aw = &self._console_messages; + const start = aw.written().len; + if (start >= max_console_bytes) return; + appendConsoleMessageInner(&aw.writer, msg) catch { + aw.shrinkRetainingCapacity(start); + }; +} + +fn appendConsoleMessageInner(w: *std.Io.Writer, msg: *const Notification.ConsoleMessage) !void { + try w.print("[{s}] ", .{@tagName(msg.type)}); + for (msg.values, 0..) |value, i| { + if (i > 0) try w.writeAll(" "); + try value.format(w); + } + try w.writeByte('\n'); +} + +/// Drains and clears the buffered console output. The returned slice is valid +/// until the next dispatched `console_message` reuses the backing storage, +/// so callers must consume or copy it before that happens. +pub fn drainConsoleMessages(self: *Session) []const u8 { + const text = self._console_messages.written(); + self._console_messages.clearRetainingCapacity(); + return text; +} + pub fn processQueuedDestroyed(self: *Session) void { for (self._queued_destroy.items) |page| { page.deinit(); diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 5b45ed7a..136e9d3c 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -1037,8 +1037,7 @@ fn lookupLpEnv(name: []const u8) ?[:0]const u8 { } fn execConsoleLogs(arena: std.mem.Allocator, session: *lp.Session) ToolError![]const u8 { - const page = try requireFrame(session); - const text = page.drainConsoleMessages(); + const text = session.drainConsoleMessages(); if (text.len == 0) return "No console messages."; return arena.dupe(u8, text) catch ToolError.InternalError; } diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index b1c6bb5e..57b6c158 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -20,7 +20,6 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); -const Frame = @import("../Frame.zig"); const Notification = @import("../../Notification.zig"); const datetime = @import("../../datetime.zig"); @@ -63,25 +62,21 @@ pub fn trace(_: *const Console, values: []js.Value, exec: *js.Execution) !void { pub fn debug(_: *const Console, values: []js.Value, exec: *js.Execution) void { logger.debug(.js, "console.debug", .{ValueWriter{ .values = values }}); - appendMessage(exec, .debug, values); dispatchConsoleMessage(values, .debug, exec); } pub fn info(_: *const Console, values: []js.Value, exec: *js.Execution) void { logger.info(.js, "console.info", .{ValueWriter{ .values = values }}); - appendMessage(exec, .info, values); dispatchConsoleMessage(values, .info, exec); } pub fn log(_: *const Console, values: []js.Value, exec: *js.Execution) void { logger.info(.js, "console.log", .{ValueWriter{ .values = values }}); - appendMessage(exec, .log, values); dispatchConsoleMessage(values, .info, exec); } pub fn warn(_: *const Console, values: []js.Value, exec: *js.Execution) void { logger.warn(.js, "console.warn", .{ValueWriter{ .values = values }}); - appendMessage(exec, .warn, values); dispatchConsoleMessage(values, .info, exec); } @@ -92,12 +87,11 @@ pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, exec: return; } logger.warn(.js, "console.assert", .{ValueWriter{ .values = values }}); - appendMessage(exec, .warn, values); + dispatchConsoleMessage(values, .warn, exec); } pub fn @"error"(_: *const Console, values: []js.Value, exec: *js.Execution) void { logger.warn(.js, "console.error", .{ValueWriter{ .values = values, .stack = exec.context.local.?.stackTrace() catch |err| @errorName(err) orelse "???" }}); - appendMessage(exec, .@"error", values); dispatchConsoleMessage(values, .@"error", exec); } @@ -178,16 +172,6 @@ fn timestamp() u64 { return @import("../../datetime.zig").timestamp(.monotonic); } -// Forwards frame-context console output to the Frame's message buffer (read by -// the `consoleLogs` tool / CDP Runtime.consoleAPICalled). Worker contexts are -// dropped — no buffer is attached there. -fn appendMessage(exec: *js.Execution, level: Frame.ConsoleLevel, values: []js.Value) void { - switch (exec.context.global) { - .frame => |f| f.appendConsoleMessage(level, values), - .worker => {}, - } -} - const ValueWriter = struct { values: []js.Value, stack: ?[]const u8 = null,