From 15f06474cfc30db568b16049dd7ffae005e59aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Wed, 13 May 2026 08:57:44 +0200 Subject: [PATCH] refactor(browser): move console log buffering to Session Moves the console message buffer from Frame to Session and populates it via the notification system. This centralizes log collection for the MCP tool and simplifies the Console Web API implementation. --- src/Config.zig | 2 +- src/browser/Frame.zig | 33 -------------------------- src/browser/Session.zig | 42 ++++++++++++++++++++++++++++++++++ src/browser/tools.zig | 3 +-- src/browser/webapi/Console.zig | 18 +-------------- 5 files changed, 45 insertions(+), 53 deletions(-) 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,