diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index f6f828e5..d7cff37c 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -911,11 +911,8 @@ const tool_output_max_bytes: usize = 1 * 1024 * 1024; fn capToolOutput(allocator: std.mem.Allocator, output: []const u8) []const u8 { if (output.len <= tool_output_max_bytes) return output; const prefix = output[0..tool_output_max_bytes]; - return std.fmt.allocPrint( - allocator, - "{s}\n...[truncated, original {d} bytes]", - .{ prefix, output.len }, - ) catch prefix; + const suffix = std.fmt.allocPrint(allocator, "\n...[truncated, original {d} bytes]", .{output.len}) catch return prefix; + return std.mem.concat(allocator, u8, &.{ prefix, suffix }) catch prefix; } fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []const u8, arguments: []const u8) zenai.provider.Client.ToolHandler.Result { diff --git a/src/agent/Command.zig b/src/agent/Command.zig index 15a9ae1a..6e2fc7e0 100644 --- a/src/agent/Command.zig +++ b/src/agent/Command.zig @@ -530,15 +530,7 @@ pub fn fromToolCallValue(tool_name: []const u8, arguments: std.json.Value) ?Comm } }, .scroll => blk: { if (obj.get("backendNodeId") != null) break :blk null; - const x: i32 = switch (obj.get("x") orelse std.json.Value{ .integer = 0 }) { - .integer => |i| @intCast(i), - else => 0, - }; - const y: i32 = switch (obj.get("y") orelse std.json.Value{ .integer = 0 }) { - .integer => |i| @intCast(i), - else => 0, - }; - break :blk .{ .scroll = .{ .x = x, .y = y } }; + break :blk .{ .scroll = .{ .x = getJsonI32(obj, "x", 0), .y = getJsonI32(obj, "y", 0) } }; }, else => null, }; @@ -551,6 +543,13 @@ fn getJsonString(o: std.json.ObjectMap, key: []const u8) ?[]const u8 { }; } +fn getJsonI32(o: std.json.ObjectMap, key: []const u8, default: i32) i32 { + return switch (o.get(key) orelse return default) { + .integer => |i| std.math.cast(i32, i) orelse default, + else => default, + }; +} + // --- Tests --- test "parse GOTO" { diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index c81c65ae..dc1c1580 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -516,8 +516,8 @@ fn formatReplResult(arena: std.mem.Allocator, name: []const u8, result: []const return aw.written(); } -pub fn printError(_: *Self, msg: []const u8) void { - std.debug.print("{s}{s}Error: {s}{s}\n", .{ ansi.bold, ansi.red, msg, ansi.reset }); +pub fn printError(self: *Self, msg: []const u8) void { + self.printErrorFmt("{s}", .{msg}); } pub fn printErrorFmt(_: *Self, comptime fmt: []const u8, args: anytype) void { diff --git a/src/agent/ToolExecutor.zig b/src/agent/ToolExecutor.zig index 48f05c9a..0acbf495 100644 --- a/src/agent/ToolExecutor.zig +++ b/src/agent/ToolExecutor.zig @@ -73,8 +73,7 @@ pub fn schemaAllocator(self: *Self) std.mem.Allocator { } pub fn getCurrentUrl(self: *Self) []const u8 { - const page = self.session.currentFrame() orelse return "(no page loaded)"; - return page.url; + return browser_tools.currentUrlOrPlaceholder(self.session); } /// Run a JavaScript expression and return the full result (text + error flag). diff --git a/src/browser/tools.zig b/src/browser/tools.zig index cd153fae..fa7bc4e5 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -948,6 +948,14 @@ fn execGetUrl(session: *lp.Session) ToolError![]const u8 { return page.url; } +/// URL of the active frame, or a stable placeholder when no page is loaded. +/// Use from contexts that just want a string for display/logging; callers +/// that need to react to "no page" should check `currentFrame()` directly. +pub fn currentUrlOrPlaceholder(session: *lp.Session) []const u8 { + const frame = session.currentFrame() orelse return "(no page loaded)"; + return frame.url; +} + fn execGetCookies(arena: std.mem.Allocator, session: *lp.Session) ToolError![]const u8 { const cookies = session.cookie_jar.cookies.items; if (cookies.len == 0) return "No cookies."; @@ -1030,6 +1038,7 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input; var result: std.ArrayList(u8) = .empty; + result.ensureTotalCapacity(arena, input.len) catch return input; var i: usize = first_lp; var last_copy: usize = 0; while (std.mem.indexOfScalarPos(u8, input, i, '$')) |dollar| { diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 1c17e094..9896e5fb 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -288,7 +288,7 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu } const result = browser_tools.call(arena, server.session, &server.node_registry, tcv.name, tcv.args) catch |err| { - const url = currentUrl(server) catch ""; + const url = browser_tools.currentUrlOrPlaceholder(server.session); const msg = std.fmt.allocPrint(arena, "{s} failed at line `{s}` (url: {s}): {s}", .{ tcv.name, args.line, url, @errorName(err) }) catch @errorName(err); return sendErrorContent(server, id, msg); }; @@ -298,7 +298,7 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu // roundtrip the same way an exec failure does. const verification = server.verifier.verify(arena, cmd); if (verification.result == .failed) { - const url = currentUrl(server) catch ""; + const url = browser_tools.currentUrlOrPlaceholder(server.session); const reason = verification.reason orelse "verification failed"; const msg = std.fmt.allocPrint(arena, "{s} executed at line `{s}` but verification failed (url: {s}): {s}", .{ tcv.name, args.line, url, reason }) catch reason; return sendErrorContent(server, id, msg); @@ -381,11 +381,6 @@ fn findLineSpan(content: []const u8, line: []const u8) error{ NotFound, Ambiguou return found orelse error.NotFound; } -fn currentUrl(server: *Server) ![]const u8 { - const frame = server.session.currentFrame() orelse return "(no page loaded)"; - return frame.url; -} - fn sendErrorContent(server: *Server, id: std.json.Value, msg: []const u8) !void { const content = [_]protocol.TextContent([]const u8){.{ .text = msg }}; try server.transport.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = true });