diff --git a/src/script.zig b/src/script.zig index aeccea99..402328c0 100644 --- a/src/script.zig +++ b/src/script.zig @@ -107,10 +107,9 @@ pub const mcp_driver_guidance = ; pub const Replacement = struct { - /// Slice into the original content buffer that should be replaced. /// Must alias into the `content` passed to `applyReplacements`. original_span: []const u8, - /// New text to substitute (caller is responsible for trailing newlines). + /// Caller is responsible for trailing newlines. new_text: []const u8, }; @@ -171,6 +170,8 @@ pub fn writeAtomic( const new_content = try applyReplacements(allocator, content, replacements); defer allocator.free(new_content); + if (std.mem.eql(u8, new_content, content)) return; + var bak_buf: [std.fs.max_path_bytes]u8 = undefined; const bak_path = try std.fmt.bufPrint(&bak_buf, "{s}.bak", .{path}); try dir.writeFile(.{ .sub_path = bak_path, .data = content }); @@ -191,9 +192,13 @@ pub fn formatHealReplacement( cmds: []const Command, ) !Replacement { std.debug.assert(cmds.len > 0); - const lines = try arena.alloc([]const u8, cmds.len); - for (cmds, 0..) |cmd, i| lines[i] = try std.fmt.allocPrint(arena, "{f}", .{cmd}); - return formatHealReplacementLines(arena, original_span, raw_line, lines); + var aw: std.Io.Writer.Allocating = .init(arena); + try aw.writer.print("# [Auto-healed] Original: {s}\n", .{raw_line}); + for (cmds) |cmd| { + try cmd.format(&aw.writer); + try aw.writer.writeByte('\n'); + } + return .{ .original_span = original_span, .new_text = aw.written() }; } /// Same shape as `formatHealReplacement` but for callers that already have diff --git a/src/script/Verifier.zig b/src/script/Verifier.zig index b4d723bf..40ef3f34 100644 --- a/src/script/Verifier.zig +++ b/src/script/Verifier.zig @@ -100,12 +100,12 @@ fn verifyElementValue(self: *Verifier, arena: std.mem.Allocator, selector: []con } fn queryElementProperty(self: *Verifier, arena: std.mem.Allocator, selector: []const u8, property: ElementProperty) ?[]const u8 { - const selector_json = std.json.Stringify.valueAlloc(arena, selector, .{}) catch return null; - const script = std.fmt.allocPrint( - arena, - "(function(){{ var el = document.querySelector({s}); return el ? {s} : null; }})()", - .{ selector_json, property.jsExpr() }, - ) catch return null; - const result = browser_tools.evalScript(arena, self.session, self.node_registry, script) catch return null; + var aw: std.Io.Writer.Allocating = .init(arena); + aw.writer.writeAll("(function(){ var el = document.querySelector(") catch return null; + std.json.Stringify.value(selector, .{}, &aw.writer) catch return null; + aw.writer.writeAll("); return el ? ") catch return null; + aw.writer.writeAll(property.jsExpr()) catch return null; + aw.writer.writeAll(" : null; })()") catch return null; + const result = browser_tools.evalScript(arena, self.session, self.node_registry, aw.written()) catch return null; return result.okText(); } diff --git a/src/script/command.zig b/src/script/command.zig index 98a16c2a..2f50388a 100644 --- a/src/script/command.zig +++ b/src/script/command.zig @@ -197,9 +197,9 @@ pub const Command = union(enum) { } } - /// Parse a line of REPL input into a PandaScript command. - /// Unrecognized input is returned as `.natural_language`. - /// For multi-line EVAL blocks in scripts, use `ScriptParser`. + /// Unrecognized input falls through to `.natural_language`; multi-line + /// `EVAL '''…'''` / `EXTRACT '''…'''` blocks need `ScriptIterator`, + /// which `parse` (line-only) can't assemble. pub fn parse(line: []const u8) Command { const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); if (trimmed.len == 0) return .{ .natural_language = trimmed }; @@ -312,9 +312,8 @@ pub const Command = union(enum) { return .{ .natural_language = trimmed }; } - /// Inverse of `toToolCall`: map an LLM tool call into a Command, or return - /// null if the tool name doesn't correspond to a PandaScript command. - /// Variants emitted by `toToolCall` round-trip through this. + /// Round-trips with `toToolCall`. Returns null for tool names outside + /// the PandaScript-emittable set. pub fn fromToolCall(tool_name: []const u8, arguments: std.json.Value) ?Command { const Action = lp.tools.Action; const action = std.meta.stringToEnum(Action, tool_name) orelse return null; @@ -433,7 +432,8 @@ pub const Command = union(enum) { .{ .name = "ACCEPT_COOKIES", .args = null }, }; - /// Iterator for parsing a script file, handling multi-line EVAL """ ... """ blocks. + /// Unlike `Command.parse`, assembles multi-line `EVAL '''…'''` and + /// `EXTRACT '''…'''` blocks into a single Command. pub const ScriptIterator = struct { allocator: std.mem.Allocator, lines: std.mem.SplitIterator(u8, .scalar), @@ -457,7 +457,6 @@ pub const Command = union(enum) { command: Command, }; - /// Multi-line EVAL / EXTRACT blocks are assembled into a single command. pub fn next(self: *ScriptIterator) std.mem.Allocator.Error!?Entry { while (self.lines.next()) |line| { self.line_num += 1;