diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index dee93271..7800b124 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -443,18 +443,17 @@ fn runScript(self: *Self, path: []const u8) bool { }; defer file.close(); - const content = file.readToEndAlloc(self.allocator, 10 * 1024 * 1024) catch |err| { - self.terminal.printErrorFmt("Failed to read script: {s}", .{@errorName(err)}); - return false; - }; - defer self.allocator.free(content); - self.terminal.printInfoFmt("Running script: {s}", .{path}); var script_arena: std.heap.ArenaAllocator = .init(self.allocator); defer script_arena.deinit(); const sa = script_arena.allocator(); + const content = file.readToEndAlloc(sa, 10 * 1024 * 1024) catch |err| { + self.terminal.printErrorFmt("Failed to read script: {s}", .{@errorName(err)}); + return false; + }; + var iter: Command.ScriptIterator = .init(sa, content); var last_comment: ?[]const u8 = null; var replacements: std.ArrayList(Replacement) = .empty; @@ -1021,8 +1020,8 @@ fn buildUserMessageParts( } var parts: std.ArrayList(zenai.provider.ContentPart) = .empty; - const combined = try std.fmt.allocPrint(ma, "{s}{s}", .{ text_prefix.items, user_input }); - try parts.append(ma, .{ .text = combined }); + try text_prefix.appendSlice(ma, user_input); + try parts.append(ma, .{ .text = try text_prefix.toOwnedSlice(ma) }); for (inline_parts.items) |p| try parts.append(ma, p); return parts.toOwnedSlice(ma); } diff --git a/src/agent/Recorder.zig b/src/agent/Recorder.zig index 4f26896c..94806f29 100644 --- a/src/agent/Recorder.zig +++ b/src/agent/Recorder.zig @@ -8,6 +8,9 @@ const Self = @This(); allocator: std.mem.Allocator, file: ?std.fs.File, needs_separator: bool, +/// Reused between `record()` calls so each command line doesn't alloc/free. +/// Cleared with `clearRetainingCapacity` instead. +buf: std.Io.Writer.Allocating, /// Append-open `path`, inserting a leading newline if the file is non-empty. /// A null path disables recording. @@ -27,10 +30,11 @@ pub fn init(allocator: std.mem.Allocator, path: ?[]const u8) Self { break :blk f; } else null; - return .{ .allocator = allocator, .file = file, .needs_separator = false }; + return .{ .allocator = allocator, .file = file, .needs_separator = false, .buf = .init(allocator) }; } pub fn deinit(self: *Self) void { + self.buf.deinit(); if (self.file) |f| f.close(); } @@ -38,11 +42,10 @@ pub fn record(self: *Self, cmd: Command.Command) void { const f = self.file orelse return; if (!cmd.isRecorded()) return; - var aw: std.Io.Writer.Allocating = .init(self.allocator); - defer aw.deinit(); - cmd.format(&aw.writer) catch return; - aw.writer.writeByte('\n') catch return; - _ = f.write(aw.written()) catch return; + self.buf.clearRetainingCapacity(); + cmd.format(&self.buf.writer) catch return; + self.buf.writer.writeByte('\n') catch return; + _ = f.write(self.buf.written()) catch return; self.needs_separator = true; } @@ -61,7 +64,7 @@ test "record writes state-mutating commands" { const file = tmp.dir.createFile("test.lp", .{ .read = true }) catch unreachable; - var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false }; + var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false, .buf = .init(std.testing.allocator) }; defer recorder.deinit(); recorder.record(Command.parse("GOTO https://example.com")); @@ -103,7 +106,7 @@ test "record skips empty and comment lines" { const file = tmp.dir.createFile("test2.lp", .{ .read = true }) catch unreachable; - var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false }; + var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false, .buf = .init(std.testing.allocator) }; defer recorder.deinit(); recorder.record(Command.parse("")); @@ -120,7 +123,7 @@ test "record skips empty and comment lines" { } test "recorder with null file is no-op" { - var recorder: Self = .{ .allocator = std.testing.allocator, .file = null, .needs_separator = false }; + var recorder: Self = .{ .allocator = std.testing.allocator, .file = null, .needs_separator = false, .buf = .init(std.testing.allocator) }; recorder.record(Command.parse("GOTO https://example.com")); recorder.recordComment("# test"); recorder.deinit(); diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 58887f15..7fc155ba 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -1011,9 +1011,11 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 // Same gate as `execGetEnv`: only `LP_*` is resolvable. A // prompt-injected `fill('$ANTHROPIC_API_KEY')` would otherwise // leak the resolved value into the page DOM. - const env_val: ?[:0]const u8 = if (std.ascii.startsWithIgnoreCase(name, "LP_")) blk: { - const name_z = arena.dupeZ(u8, name) catch return input; - break :blk std.posix.getenv(name_z); + var name_buf: [256]u8 = undefined; + const env_val: ?[:0]const u8 = if (std.ascii.startsWithIgnoreCase(name, "LP_") and name.len < name_buf.len) blk: { + @memcpy(name_buf[0..name.len], name); + name_buf[name.len] = 0; + break :blk std.posix.getenv(name_buf[0..name.len :0]); } else null; if (env_val) |val| { result.appendSlice(arena, val) catch return input;